diff --git a/.appveyor.yml b/.appveyor.yml index 0a93da90..49ee4709 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,5 +1,5 @@ version: '2.0.0-develop-{build}' -image: Visual Studio 2017 +image: Visual Studio 2019 before_build: - nuget restore Parse.sln build_script: @@ -9,7 +9,7 @@ before_test: - choco install opencover.portable - choco install codecov test_script: - - OpenCover.Console.exe -target:dotnet.exe -targetargs:"test --test-adapter-path:. --logger:Appveyor /p:DebugType=full .\Parse.Test\Parse.Test.csproj" -filter:"+[Parse*]* -[Parse.Test*]*" -oldstyle -output:parse_sdk_dotnet_coverage.xml -register:user + - OpenCover.Console.exe -target:dotnet.exe -targetargs:"test --test-adapter-path:. --logger:Appveyor /p:DebugType=full .\Parse.Tests\Parse.Tests.csproj" -filter:"+[Parse*]* -[Parse.Tests*]*" -oldstyle -output:parse_sdk_dotnet_coverage.xml -register:user after_test: - codecov -f "parse_sdk_dotnet_coverage.xml" artifacts: diff --git a/.editorconfig b/.editorconfig index ef749478..330d330b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -81,4 +81,16 @@ csharp_space_between_method_call_parameter_list_parentheses = false csharp_space_between_parentheses = false csharp_preserve_single_line_statements = false -csharp_preserve_single_line_blocks = true \ No newline at end of file +csharp_preserve_single_line_blocks = true + +# CC0005: Empty Object Initializer +dotnet_diagnostic.CC0005.severity = none + +# CC0105: You should use 'var' whenever possible. +dotnet_diagnostic.CC0105.severity = none + +# CC0001: You should use 'var' whenever possible. +dotnet_diagnostic.CC0001.severity = none + +# CC0057: Unused parameters +dotnet_diagnostic.CC0057.severity = none \ No newline at end of file diff --git a/Parse.Test/AnalyticsTests.cs b/Parse.Test/AnalyticsTests.cs deleted file mode 100644 index ca4cd3d4..00000000 --- a/Parse.Test/AnalyticsTests.cs +++ /dev/null @@ -1,98 +0,0 @@ -using Moq; -using Parse; -using Parse.Analytics.Internal; -using Parse.Core.Internal; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Parse.Test -{ - [TestClass] - public class AnalyticsTests - { - [TestCleanup] - public void TearDown() => ParseAnalyticsPlugins.Instance.Reset(); - - [TestMethod] - [AsyncStateMachine(typeof(AnalyticsTests))] - public Task TestTrackEvent() - { - Mock mockController = new Mock(); - Mock mockCorePlugins = new Mock(); - Mock mockCurrentUserController = new Mock(); - - mockCorePlugins.Setup(corePlugins => corePlugins.CurrentUserController).Returns(mockCurrentUserController.Object); - - mockCurrentUserController.Setup(controller => controller.GetCurrentSessionTokenAsync(It.IsAny())).Returns(Task.FromResult("sessionToken")); - - ParseAnalyticsPlugins.Instance = new ParseAnalyticsPlugins - { - AnalyticsController = mockController.Object, - CorePlugins = mockCorePlugins.Object - }; - - return ParseAnalytics.TrackEventAsync("SomeEvent").ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - mockController.Verify(obj => obj.TrackEventAsync(It.Is(eventName => eventName == "SomeEvent"), It.Is>(dict => dict == null), It.IsAny(), It.IsAny()), Times.Exactly(1)); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(AnalyticsTests))] - public Task TestTrackEventWithDimension() - { - Mock mockController = new Mock(); - Mock mockCorePlugins = new Mock(); - Mock mockCurrentUserController = new Mock(); - - mockCorePlugins.Setup(corePlugins => corePlugins.CurrentUserController).Returns(mockCurrentUserController.Object); - - mockCurrentUserController.Setup(controller => controller.GetCurrentSessionTokenAsync(It.IsAny())).Returns(Task.FromResult("sessionToken")); - - ParseAnalyticsPlugins.Instance = new ParseAnalyticsPlugins - { - AnalyticsController = mockController.Object, - CorePlugins = mockCorePlugins.Object - }; - - return ParseAnalytics.TrackEventAsync("SomeEvent", new Dictionary { ["facebook"] = "hq" }).ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - mockController.Verify(obj => obj.TrackEventAsync(It.Is(eventName => eventName == "SomeEvent"), It.Is>(dict => dict != null && dict.Count == 1), It.IsAny(), It.IsAny()), Times.Exactly(1)); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(AnalyticsTests))] - public Task TestTrackAppOpened() - { - Mock mockController = new Mock(); - Mock mockCorePlugins = new Mock(); - Mock mockCurrentUserController = new Mock(); - - mockCorePlugins.Setup(corePlugins => corePlugins.CurrentUserController).Returns(mockCurrentUserController.Object); - - mockCurrentUserController.Setup(controller => controller.GetCurrentSessionTokenAsync(It.IsAny())).Returns(Task.FromResult("sessionToken")); - - ParseAnalyticsPlugins.Instance = new ParseAnalyticsPlugins - { - AnalyticsController = mockController.Object, - CorePlugins = mockCorePlugins.Object - }; - - return ParseAnalytics.TrackAppOpenedAsync().ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - mockController.Verify(obj => obj.TrackAppOpenedAsync(It.Is(pushHash => pushHash == null), It.IsAny(), It.IsAny()), Times.Exactly(1)); - }); - } - } -} diff --git a/Parse.Test/CloudControllerTests.cs b/Parse.Test/CloudControllerTests.cs deleted file mode 100644 index 3c76b121..00000000 --- a/Parse.Test/CloudControllerTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Moq; -using Parse; -using Parse.Core.Internal; -using System; -using System.Collections.Generic; -using System.Net; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Parse.Test -{ - [TestClass] - public class CloudControllerTests - { - [TestInitialize] - public void SetUp() => ParseClient.Initialize(new ParseClient.Configuration { ApplicationID = "", Key = "" }); - - [TestMethod] - [AsyncStateMachine(typeof(CloudControllerTests))] - public Task TestEmptyCallFunction() => new ParseCloudCodeController(CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, null)).Object).CallFunctionAsync("someFunction", null, null, CancellationToken.None).ContinueWith(t => - { - Assert.IsTrue(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - }); - - [TestMethod] - [AsyncStateMachine(typeof(CloudControllerTests))] - public Task TestCallFunction() - { - Dictionary responseDict = new Dictionary { ["result"] = "gogo" }; - Tuple> response = new Tuple>(HttpStatusCode.Accepted, responseDict); - Mock mockRunner = CreateMockRunner(response); - - ParseCloudCodeController controller = new ParseCloudCodeController(mockRunner.Object); - return controller.CallFunctionAsync("someFunction", null, null, CancellationToken.None).ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - Assert.AreEqual("gogo", t.Result); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(CloudControllerTests))] - public Task TestCallFunctionWithComplexType() => new ParseCloudCodeController(CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, new Dictionary() { { "result", new Dictionary { { "fosco", "ben" }, { "list", new List { 1, 2, 3 } } } } })).Object).CallFunctionAsync>("someFunction", null, null, CancellationToken.None).ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - Assert.IsInstanceOfType(t.Result, typeof(IDictionary)); - Assert.AreEqual("ben", t.Result["fosco"]); - Assert.IsInstanceOfType(t.Result["list"], typeof(IList)); - }); - - [TestMethod] - [AsyncStateMachine(typeof(CloudControllerTests))] - public Task TestCallFunctionWithWrongType() => new ParseCloudCodeController(this.CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, new Dictionary() { { "result", "gogo" } })).Object).CallFunctionAsync("someFunction", null, null, CancellationToken.None).ContinueWith(t => - { - Assert.IsTrue(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - }); - - private Mock CreateMockRunner(Tuple> response) - { - Mock mockRunner = new Mock { }; - mockRunner.Setup(obj => obj.RunCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(response)); - - return mockRunner; - } - } -} diff --git a/Parse.Test/CloudTests.cs b/Parse.Test/CloudTests.cs deleted file mode 100644 index de373103..00000000 --- a/Parse.Test/CloudTests.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Moq; -using Parse; -using Parse.Core.Internal; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Parse.Test -{ - [TestClass] - public class CloudTests - { - [TestCleanup] - public void TearDown() => ParseCorePlugins.Instance.Reset(); - - [TestMethod] - [AsyncStateMachine(typeof(CloudTests))] - public Task TestCloudFunctions() - { - Mock mockController = new Mock(); - mockController.Setup(obj => obj.CallFunctionAsync>(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny())).Returns(Task.FromResult>(new Dictionary { ["fosco"] = "ben", ["list"] = new List { 1, 2, 3 } })); - - ParseCorePlugins.Instance = new ParseCorePlugins - { - CloudCodeController = mockController.Object, - CurrentUserController = new Mock().Object - }; - - return ParseCloud.CallFunctionAsync>("someFunction", null, CancellationToken.None).ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - Assert.IsInstanceOfType(t.Result, typeof(IDictionary)); - Assert.AreEqual("ben", t.Result["fosco"]); - Assert.IsInstanceOfType(t.Result["list"], typeof(IList)); - }); - } - } -} diff --git a/Parse.Test/CommandTests.cs b/Parse.Test/CommandTests.cs deleted file mode 100644 index f46980bb..00000000 --- a/Parse.Test/CommandTests.cs +++ /dev/null @@ -1,136 +0,0 @@ -using Moq; -using Parse; -using Parse.Common.Internal; -using Parse.Core.Internal; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Parse.Test -{ - [TestClass] - public class CommandTests - { - [TestInitialize] - public void SetUp() => ParseClient.Initialize(new ParseClient.Configuration { ApplicationID = "", Key = "" }); - - [TestCleanup] - public void TearDown() => ParseCorePlugins.Instance.Reset(); - - [TestMethod] - public void TestMakeCommand() - { - ParseCommand command = new ParseCommand("endpoint", method: "GET", sessionToken: "abcd", headers: null, data: null); - - Assert.AreEqual("/1/endpoint", command.Uri.AbsolutePath); - Assert.AreEqual("GET", command.Method); - Assert.IsTrue(command.Headers.Any(pair => pair.Key == "X-Parse-Session-Token" && pair.Value == "abcd")); - } - - [TestMethod] - [AsyncStateMachine(typeof(CommandTests))] - public Task TestRunCommand() - { - Mock mockHttpClient = new Mock(); - Mock mockInstallationIdController = new Mock(); - Task> fakeResponse = Task.FromResult(new Tuple(HttpStatusCode.OK, "{}")); - mockHttpClient.Setup(obj => obj.ExecuteAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(fakeResponse); - - mockInstallationIdController.Setup(i => i.GetAsync()).Returns(Task.FromResult(null)); - - return new ParseCommandRunner(mockHttpClient.Object, mockInstallationIdController.Object).RunCommandAsync(new ParseCommand("endpoint", method: "GET", data: null)).ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - Assert.IsInstanceOfType(t.Result.Item2, typeof(IDictionary)); - Assert.AreEqual(0, t.Result.Item2.Count); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(CommandTests))] - public Task TestRunCommandWithArrayResult() - { - Mock mockHttpClient = new Mock(); - Mock mockInstallationIdController = new Mock(); - mockHttpClient.Setup(obj => obj.ExecuteAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(new Tuple(HttpStatusCode.OK, "[]"))); - - mockInstallationIdController.Setup(i => i.GetAsync()).Returns(Task.FromResult(null)); - - return new ParseCommandRunner(mockHttpClient.Object, mockInstallationIdController.Object).RunCommandAsync(new ParseCommand("endpoint", method: "GET", data: null)).ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - Assert.IsInstanceOfType(t.Result.Item2, typeof(IDictionary)); - Assert.AreEqual(1, t.Result.Item2.Count); - Assert.IsTrue(t.Result.Item2.ContainsKey("results")); - Assert.IsInstanceOfType(t.Result.Item2["results"], typeof(IList)); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(CommandTests))] - public Task TestRunCommandWithInvalidString() - { - Mock mockHttpClient = new Mock(); - Mock mockInstallationIdController = new Mock(); - mockHttpClient.Setup(obj => obj.ExecuteAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(new Tuple(HttpStatusCode.OK, "invalid"))); - - mockInstallationIdController.Setup(i => i.GetAsync()).Returns(Task.FromResult(null)); - - return new ParseCommandRunner(mockHttpClient.Object, mockInstallationIdController.Object).RunCommandAsync(new ParseCommand("endpoint", method: "GET", data: null)).ContinueWith(t => - { - Assert.IsTrue(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - Assert.IsInstanceOfType(t.Exception.InnerException, typeof(ParseException)); - Assert.AreEqual(ParseException.ErrorCode.OtherCause, (t.Exception.InnerException as ParseException).Code); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(CommandTests))] - public Task TestRunCommandWithErrorCode() - { - Mock mockHttpClient = new Mock(); - Mock mockInstallationIdController = new Mock(); - mockHttpClient.Setup(obj => obj.ExecuteAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(new Tuple(HttpStatusCode.NotFound, "{ \"code\": 101, \"error\": \"Object not found.\" }"))); - - mockInstallationIdController.Setup(i => i.GetAsync()).Returns(Task.FromResult(null)); - - return new ParseCommandRunner(mockHttpClient.Object, mockInstallationIdController.Object).RunCommandAsync(new ParseCommand("endpoint", method: "GET", data: null)).ContinueWith(t => - { - Assert.IsTrue(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - Assert.IsInstanceOfType(t.Exception.InnerException, typeof(ParseException)); - ParseException parseException = t.Exception.InnerException as ParseException; - Assert.AreEqual(ParseException.ErrorCode.ObjectNotFound, parseException.Code); - Assert.AreEqual("Object not found.", parseException.Message); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(CommandTests))] - public Task TestRunCommandWithInternalServerError() - { - Mock mockHttpClient = new Mock(); - Mock mockInstallationIdController = new Mock(); - mockHttpClient.Setup(obj => obj.ExecuteAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(new Tuple(HttpStatusCode.InternalServerError, null))); - - mockInstallationIdController.Setup(i => i.GetAsync()).Returns(Task.FromResult(null)); - - return new ParseCommandRunner(mockHttpClient.Object, mockInstallationIdController.Object).RunCommandAsync(new ParseCommand("endpoint", method: "GET", data: null)).ContinueWith(t => - { - Assert.IsTrue(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - Assert.IsInstanceOfType(t.Exception.InnerException, typeof(ParseException)); - Assert.AreEqual(ParseException.ErrorCode.InternalServerError, (t.Exception.InnerException as ParseException).Code); - }); - } - } -} diff --git a/Parse.Test/ConfigTests.cs b/Parse.Test/ConfigTests.cs deleted file mode 100644 index 172a4777..00000000 --- a/Parse.Test/ConfigTests.cs +++ /dev/null @@ -1,82 +0,0 @@ -using Moq; -using Parse; -using Parse.Common.Internal; -using Parse.Core.Internal; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using Newtonsoft.Json; - -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; - -namespace Parse.Test -{ - [TestClass] - public class ConfigTests - { - private IParseConfigController MockedConfigController - { - get - { - Mock mockedConfigController = new Mock(); - Mock mockedCurrentConfigController = new Mock(); - - ParseConfig theConfig = ParseConfigExtensions.Create(new Dictionary { ["params"] = new Dictionary { ["testKey"] = "testValue" } }); - - mockedCurrentConfigController.Setup(obj => obj.GetCurrentConfigAsync()).Returns(Task.FromResult(theConfig)); - - mockedConfigController.Setup(obj => obj.CurrentConfigController).Returns(mockedCurrentConfigController.Object); - - TaskCompletionSource tcs = new TaskCompletionSource(); - tcs.TrySetCanceled(); - - mockedConfigController.Setup(obj => obj.FetchConfigAsync(It.IsAny(), It.Is(ct => ct.IsCancellationRequested))).Returns(tcs.Task); - - mockedConfigController.Setup(obj => obj.FetchConfigAsync(It.IsAny(), It.Is(ct => !ct.IsCancellationRequested))).Returns(Task.FromResult(theConfig)); - - return mockedConfigController.Object; - } - } - - [TestInitialize] - public void SetUp() => ParseCorePlugins.Instance = new ParseCorePlugins { ConfigController = MockedConfigController, CurrentUserController = new Mock().Object }; - - [TestCleanup] - public void TearDown() => ParseCorePlugins.Instance = null; - - [TestMethod] - public void TestCurrentConfig() - { - ParseConfig config = ParseConfig.CurrentConfig; - - Assert.AreEqual("testValue", config["testKey"]); - Assert.AreEqual("testValue", config.Get("testKey")); - } - - [TestMethod] - public void TestToJSON() - { - IDictionary expectedJson = new Dictionary { { "params", new Dictionary { { "testKey", "testValue" } } } }; - Assert.AreEqual(JsonConvert.SerializeObject((ParseConfig.CurrentConfig as IJsonConvertible).ToJSON()), JsonConvert.SerializeObject(expectedJson)); - } - - [TestMethod] - [AsyncStateMachine(typeof(ConfigTests))] - public Task TestGetConfig() => ParseConfig.GetAsync().ContinueWith(t => - { - Assert.AreEqual("testValue", t.Result["testKey"]); - Assert.AreEqual("testValue", t.Result.Get("testKey")); - }); - - [TestMethod] - [AsyncStateMachine(typeof(ConfigTests))] - public Task TestGetConfigCancel() - { - CancellationTokenSource tokenSource = new CancellationTokenSource(); - tokenSource.Cancel(); - return ParseConfig.GetAsync(tokenSource.Token).ContinueWith(t => Assert.IsTrue(t.IsCanceled)); - } - } -} diff --git a/Parse.Test/ProgressTests.cs b/Parse.Test/ProgressTests.cs deleted file mode 100644 index 5c29f383..00000000 --- a/Parse.Test/ProgressTests.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Moq; -using Parse; -using System; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Parse.Test -{ - [TestClass] - public class ProgressTests - { - [TestMethod] - public void TestDownloadProgressEventGetterSetter() - { - ParseDownloadProgressEventArgs downloadProgressEvent = new ParseDownloadProgressEventArgs { Progress = 0.5f }; - Assert.AreEqual(0.5f, downloadProgressEvent.Progress); - - downloadProgressEvent.Progress = 1.0f; - Assert.AreEqual(1.0f, downloadProgressEvent.Progress); - } - - [TestMethod] - public void TestUploadProgressEventGetterSetter() - { - ParseDownloadProgressEventArgs uploadProgressEvent = new ParseDownloadProgressEventArgs { Progress = 0.5f }; - Assert.AreEqual(0.5f, uploadProgressEvent.Progress); - - uploadProgressEvent.Progress = 1.0f; - Assert.AreEqual(1.0f, uploadProgressEvent.Progress); - } - - [TestMethod] - public void TestObservingDownloadProgress() - { - int called = 0; - Mock> mockProgress = new Mock>(); - mockProgress.Setup(obj => obj.Report(It.IsAny())).Callback(() => called++); - IProgress progress = mockProgress.Object; - - progress.Report(new ParseDownloadProgressEventArgs { Progress = 0.2f }); - progress.Report(new ParseDownloadProgressEventArgs { Progress = 0.42f }); - progress.Report(new ParseDownloadProgressEventArgs { Progress = 0.53f }); - progress.Report(new ParseDownloadProgressEventArgs { Progress = 0.68f }); - progress.Report(new ParseDownloadProgressEventArgs { Progress = 0.88f }); - - Assert.AreEqual(5, called); - } - - [TestMethod] - public void TestObservingUploadProgress() - { - int called = 0; - Mock> mockProgress = new Mock>(); - mockProgress.Setup(obj => obj.Report(It.IsAny())).Callback(() => called++); - IProgress progress = mockProgress.Object; - - progress.Report(new ParseUploadProgressEventArgs { Progress = 0.2f }); - progress.Report(new ParseUploadProgressEventArgs { Progress = 0.42f }); - progress.Report(new ParseUploadProgressEventArgs { Progress = 0.53f }); - progress.Report(new ParseUploadProgressEventArgs { Progress = 0.68f }); - progress.Report(new ParseUploadProgressEventArgs { Progress = 0.88f }); - - Assert.AreEqual(5, called); - } - } -} diff --git a/Parse.Test/PushTests.cs b/Parse.Test/PushTests.cs deleted file mode 100644 index 646b8429..00000000 --- a/Parse.Test/PushTests.cs +++ /dev/null @@ -1,166 +0,0 @@ -using Moq; -using Parse; -using Parse.Common.Internal; -using Parse.Core.Internal; -using Parse.Push.Internal; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Parse.Test -{ - [TestClass] - public class PushTests - { - private IParsePushController GetMockedPushController(IPushState expectedPushState) - { - Mock mockedController = new Mock(MockBehavior.Strict); - - mockedController.Setup( - obj => obj.SendPushNotificationAsync( - It.Is(s => s.Equals(expectedPushState)), - It.IsAny() - ) - ).Returns(Task.FromResult(false)); - - return mockedController.Object; - } - - private IParsePushChannelsController GetMockedPushChannelsController(IEnumerable channels) - { - Mock mockedChannelsController = new Mock(MockBehavior.Strict); - - mockedChannelsController.Setup( - obj => obj.SubscribeAsync( - It.Is>(it => it.CollectionsEqual(channels)), - It.IsAny() - ) - ).Returns(Task.FromResult(false)); - - mockedChannelsController.Setup( - obj => obj.UnsubscribeAsync( - It.Is>(it => it.CollectionsEqual(channels)), - It.IsAny() - ) - ).Returns(Task.FromResult(false)); - - return mockedChannelsController.Object; - } - - [TestCleanup] - public void TearDown() - { - ParseCorePlugins.Instance = null; - ParsePushPlugins.Instance = null; - } - - [TestMethod] - [AsyncStateMachine(typeof(PushTests))] - public Task TestSendPush() - { - MutablePushState state = new MutablePushState - { - Query = ParseInstallation.Query - }; - - ParsePush thePush = new ParsePush(); - ParsePushPlugins.Instance = new ParsePushPlugins - { - PushController = GetMockedPushController(state) - }; - - thePush.Alert = "Alert"; - state.Alert = "Alert"; - - return thePush.SendAsync().ContinueWith(t => - { - Assert.IsTrue(t.IsCompleted); - Assert.IsFalse(t.IsFaulted); - - thePush.Channels = new List { { "channel" } }; - state.Channels = new List { { "channel" } }; - - return thePush.SendAsync(); - }).Unwrap().ContinueWith(t => - { - Assert.IsTrue(t.IsCompleted); - Assert.IsFalse(t.IsFaulted); - - ParseQuery query = new ParseQuery("aClass"); - thePush.Query = query; - state.Query = query; - - return thePush.SendAsync(); - }).Unwrap().ContinueWith(t => - { - Assert.IsTrue(t.IsCompleted); - Assert.IsFalse(t.IsFaulted); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(PushTests))] - public Task TestSubscribe() - { - List channels = new List(); - ParsePushPlugins.Instance = new ParsePushPlugins - { - PushChannelsController = GetMockedPushChannelsController(channels) - }; - - channels.Add("test"); - return ParsePush.SubscribeAsync("test").ContinueWith(t => - { - Assert.IsTrue(t.IsCompleted); - Assert.IsFalse(t.IsFaulted); - - return ParsePush.SubscribeAsync(new List { { "test" } }); - }).Unwrap().ContinueWith(t => - { - Assert.IsTrue(t.IsCompleted); - Assert.IsFalse(t.IsFaulted); - - CancellationTokenSource cts = new CancellationTokenSource(); - return ParsePush.SubscribeAsync(new List { { "test" } }, cts.Token); - }).Unwrap().ContinueWith(t => - { - Assert.IsTrue(t.IsCompleted); - Assert.IsFalse(t.IsFaulted); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(PushTests))] - public Task TestUnsubscribe() - { - List channels = new List(); - ParsePushPlugins.Instance = new ParsePushPlugins - { - PushChannelsController = GetMockedPushChannelsController(channels) - }; - - channels.Add("test"); - return ParsePush.UnsubscribeAsync("test").ContinueWith(t => - { - Assert.IsTrue(t.IsCompleted); - Assert.IsFalse(t.IsFaulted); - - return ParsePush.UnsubscribeAsync(new List { { "test" } }); - }).ContinueWith(t => - { - Assert.IsTrue(t.IsCompleted); - Assert.IsFalse(t.IsFaulted); - - CancellationTokenSource cts = new CancellationTokenSource(); - return ParsePush.UnsubscribeAsync(new List { { "test" } }, cts.Token); - }).ContinueWith(t => - { - Assert.IsTrue(t.IsCompleted); - Assert.IsFalse(t.IsFaulted); - }); - } - } -} diff --git a/Parse.Test/SessionControllerTests.cs b/Parse.Test/SessionControllerTests.cs deleted file mode 100644 index 1a8e4bf0..00000000 --- a/Parse.Test/SessionControllerTests.cs +++ /dev/null @@ -1,136 +0,0 @@ -using Moq; -using Parse; -using Parse.Core.Internal; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Parse.Test -{ - [TestClass] - public class SessionControllerTests - { - [TestInitialize] - public void SetUp() => ParseClient.Initialize(new ParseClient.Configuration { ApplicationID = "", Key = "" }); - - [TestMethod] - [AsyncStateMachine(typeof(SessionControllerTests))] - public Task TestGetSessionWithEmptyResult() - { - return new ParseSessionController(this.CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, null)).Object).GetSessionAsync("S0m3Se551on", CancellationToken.None).ContinueWith(t => - { - Assert.IsTrue(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(SessionControllerTests))] - public Task TestGetSession() - { - Tuple> response = new Tuple>(HttpStatusCode.Accepted, - new Dictionary() { - { "__type", "Object" }, - { "className", "Session" }, - { "sessionToken", "S0m3Se551on" }, - { "restricted", true } - }); - Mock mockRunner = CreateMockRunner(response); - - return new ParseSessionController(mockRunner.Object).GetSessionAsync("S0m3Se551on", CancellationToken.None).ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Uri.AbsolutePath == "/1/sessions/me"), - It.IsAny>(), - It.IsAny>(), - It.IsAny()), Times.Exactly(1)); - - IObjectState session = t.Result; - Assert.AreEqual(2, session.Count()); - Assert.IsTrue((bool) session["restricted"]); - Assert.AreEqual("S0m3Se551on", session["sessionToken"]); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(SessionControllerTests))] - public Task TestRevoke() - { - Tuple> response = new Tuple>(HttpStatusCode.Accepted, null); - Mock mockRunner = CreateMockRunner(response); - - ParseSessionController controller = new ParseSessionController(mockRunner.Object); - return controller.RevokeAsync("S0m3Se551on", CancellationToken.None).ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Uri.AbsolutePath == "/1/logout"), - It.IsAny>(), - It.IsAny>(), - It.IsAny()), Times.Exactly(1)); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(SessionControllerTests))] - public Task TestUpgradeToRevocableSession() - { - Tuple> response = new Tuple>(HttpStatusCode.Accepted, - new Dictionary() { - { "__type", "Object" }, - { "className", "Session" }, - { "sessionToken", "S0m3Se551on" }, - { "restricted", true } - }); - Mock mockRunner = CreateMockRunner(response); - - ParseSessionController controller = new ParseSessionController(mockRunner.Object); - return controller.UpgradeToRevocableSessionAsync("S0m3Se551on", CancellationToken.None).ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Uri.AbsolutePath == "/1/upgradeToRevocableSession"), - It.IsAny>(), - It.IsAny>(), - It.IsAny()), Times.Exactly(1)); - - IObjectState session = t.Result; - Assert.AreEqual(2, session.Count()); - Assert.IsTrue((bool) session["restricted"]); - Assert.AreEqual("S0m3Se551on", session["sessionToken"]); - }); - } - - [TestMethod] - public void TestIsRevocableSessionToken() - { - IParseSessionController sessionController = new ParseSessionController(Mock.Of()); - Assert.IsTrue(sessionController.IsRevocableSessionToken("r:session")); - Assert.IsTrue(sessionController.IsRevocableSessionToken("r:session:r:")); - Assert.IsTrue(sessionController.IsRevocableSessionToken("session:r:")); - Assert.IsFalse(sessionController.IsRevocableSessionToken("session:s:d:r")); - Assert.IsFalse(sessionController.IsRevocableSessionToken("s:ession:s:d:r")); - Assert.IsFalse(sessionController.IsRevocableSessionToken("")); - } - - - private Mock CreateMockRunner(Tuple> response) - { - Mock mockRunner = new Mock(); - mockRunner.Setup(obj => obj.RunCommandAsync(It.IsAny(), - It.IsAny>(), - It.IsAny>(), - It.IsAny())) - .Returns(Task.FromResult(response)); - - return mockRunner; - } - } -} diff --git a/Parse.Test/SessionTests.cs b/Parse.Test/SessionTests.cs deleted file mode 100644 index a7ddb7fc..00000000 --- a/Parse.Test/SessionTests.cs +++ /dev/null @@ -1,158 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using Parse; -using Parse.Core.Internal; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; - -namespace Parse.Test -{ - [TestClass] - public class SessionTests - { - [TestInitialize] - public void SetUp() - { - ParseObject.RegisterSubclass(); - ParseObject.RegisterSubclass(); - } - - [TestCleanup] - public void TearDown() => ParseCorePlugins.Instance.Reset(); - - [TestMethod] - public void TestGetSessionQuery() => Assert.IsInstanceOfType(ParseSession.Query, typeof(ParseQuery)); - - [TestMethod] - public void TestGetSessionToken() - { - ParseSession session = ParseObjectExtensions.FromState(new MutableObjectState { ServerData = new Dictionary() { { "sessionToken", "llaKcolnu" } } }, "_Session"); - Assert.IsNotNull(session); - Assert.AreEqual("llaKcolnu", session.SessionToken); - } - - [TestMethod] - [AsyncStateMachine(typeof(SessionTests))] - public Task TestGetCurrentSession() - { - IObjectState sessionState = new MutableObjectState - { - ServerData = new Dictionary() { - { "sessionToken", "newllaKcolnu" } - } - }; - Mock mockController = new Mock(); - mockController.Setup(obj => obj.GetSessionAsync(It.IsAny(), - It.IsAny())).Returns(Task.FromResult(sessionState)); - - IObjectState userState = new MutableObjectState - { - ServerData = new Dictionary() { - { "sessionToken", "llaKcolnu" } - } - }; - ParseUser user = ParseObjectExtensions.FromState(userState, "_User"); - Mock mockCurrentUserController = new Mock(); - mockCurrentUserController.Setup(obj => obj.GetAsync(It.IsAny())) - .Returns(Task.FromResult(user)); - ParseCorePlugins.Instance = new ParseCorePlugins - { - SessionController = mockController.Object, - CurrentUserController = mockCurrentUserController.Object, - }; - ParseObject.RegisterSubclass(); - - return ParseSession.GetCurrentSessionAsync().ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - mockController.Verify(obj => obj.GetSessionAsync(It.Is(sessionToken => sessionToken == "llaKcolnu"), - It.IsAny()), Times.Exactly(1)); - - ParseSession session = t.Result; - Assert.AreEqual("newllaKcolnu", session.SessionToken); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(SessionTests))] - public Task TestGetCurrentSessionWithNoCurrentUser() - { - Mock mockController = new Mock(); - Mock mockCurrentUserController = new Mock(); - ParseCorePlugins.Instance = new ParseCorePlugins - { - SessionController = mockController.Object, - CurrentUserController = mockCurrentUserController.Object, - }; - - return ParseSession.GetCurrentSessionAsync().ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - Assert.IsNull(t.Result); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(SessionTests))] - public Task TestRevoke() - { - Mock mockController = new Mock(); - mockController - .Setup(sessionController => sessionController.IsRevocableSessionToken(It.IsAny())) - .Returns(true); - - ParseCorePlugins.Instance = new ParseCorePlugins - { - SessionController = mockController.Object - }; - - CancellationTokenSource source = new CancellationTokenSource(); - return ParseSessionExtensions.RevokeAsync("r:someSession", source.Token).ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - mockController.Verify(obj => obj.RevokeAsync(It.Is(sessionToken => sessionToken == "r:someSession"), - source.Token), Times.Exactly(1)); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(SessionTests))] - public Task TestUpgradeToRevocableSession() - { - IObjectState state = new MutableObjectState - { - ServerData = new Dictionary() { - { "sessionToken", "llaKcolnu" } - } - }; - Mock mockController = new Mock(); - mockController.Setup(obj => obj.UpgradeToRevocableSessionAsync(It.IsAny(), - It.IsAny())).Returns(Task.FromResult(state)); - - Mock mockCurrentUserController = new Mock(); - ParseCorePlugins.Instance = new ParseCorePlugins - { - SessionController = mockController.Object, - CurrentUserController = mockCurrentUserController.Object, - }; - ParseObject.RegisterSubclass(); - ParseObject.RegisterSubclass(); - - CancellationTokenSource source = new CancellationTokenSource(); - return ParseSessionExtensions.UpgradeToRevocableSessionAsync("someSession", source.Token).ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - mockController.Verify(obj => obj.UpgradeToRevocableSessionAsync(It.Is(sessionToken => sessionToken == "someSession"), - source.Token), Times.Exactly(1)); - - Assert.AreEqual("llaKcolnu", t.Result); - }); - } - } -} diff --git a/Parse.Test/UserControllerTests.cs b/Parse.Test/UserControllerTests.cs deleted file mode 100644 index 49d25d7c..00000000 --- a/Parse.Test/UserControllerTests.cs +++ /dev/null @@ -1,197 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using Parse; -using Parse.Core.Internal; -using System; -using System.Collections.Generic; -using System.Net; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; - -namespace Parse.Test -{ - [TestClass] - public class UserControllerTests - { - [TestInitialize] - public void SetUp() => ParseClient.Initialize(new ParseClient.Configuration { ApplicationID = "", Key = "" }); - - [TestMethod] - [AsyncStateMachine(typeof(UserControllerTests))] - public Task TestSignUp() - { - MutableObjectState state = new MutableObjectState - { - ClassName = "_User", - ServerData = new Dictionary() { - { "username", "hallucinogen" }, - { "password", "secret" } - } - }; - Dictionary operations = new Dictionary() { - { "gogo", new Mock().Object } - }; - - Dictionary responseDict = new Dictionary() { - { "__type", "Object" }, - { "className", "_User" }, - { "objectId", "d3ImSh3ki" }, - { "sessionToken", "s3ss10nt0k3n" }, - { "createdAt", "2015-09-18T18:11:28.943Z" } - }; - Tuple> response = new Tuple>(HttpStatusCode.Accepted, responseDict); - Mock mockRunner = CreateMockRunner(response); - - ParseUserController controller = new ParseUserController(mockRunner.Object); - return controller.SignUpAsync(state, operations, CancellationToken.None).ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Uri.AbsolutePath == "/1/classes/_User"), - It.IsAny>(), - It.IsAny>(), - It.IsAny()), Times.Exactly(1)); - - IObjectState newState = t.Result; - Assert.AreEqual("s3ss10nt0k3n", newState["sessionToken"]); - Assert.AreEqual("d3ImSh3ki", newState.ObjectId); - Assert.IsNotNull(newState.CreatedAt); - Assert.IsNotNull(newState.UpdatedAt); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserControllerTests))] - public Task TestLogInWithUsernamePassword() - { - Dictionary responseDict = new Dictionary() { - { "__type", "Object" }, - { "className", "_User" }, - { "objectId", "d3ImSh3ki" }, - { "sessionToken", "s3ss10nt0k3n" }, - { "createdAt", "2015-09-18T18:11:28.943Z" } - }; - Tuple> response = new Tuple>(HttpStatusCode.Accepted, responseDict); - Mock mockRunner = CreateMockRunner(response); - - ParseUserController controller = new ParseUserController(mockRunner.Object); - return controller.LogInAsync("grantland", "123grantland123", CancellationToken.None).ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Uri.AbsolutePath == "/1/login"), - It.IsAny>(), - It.IsAny>(), - It.IsAny()), Times.Exactly(1)); - - IObjectState newState = t.Result; - Assert.AreEqual("s3ss10nt0k3n", newState["sessionToken"]); - Assert.AreEqual("d3ImSh3ki", newState.ObjectId); - Assert.IsNotNull(newState.CreatedAt); - Assert.IsNotNull(newState.UpdatedAt); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserControllerTests))] - public Task TestLogInWithAuthData() - { - Dictionary responseDict = new Dictionary() { - { "__type", "Object" }, - { "className", "_User" }, - { "objectId", "d3ImSh3ki" }, - { "sessionToken", "s3ss10nt0k3n" }, - { "createdAt", "2015-09-18T18:11:28.943Z" } - }; - Tuple> response = new Tuple>(HttpStatusCode.Accepted, responseDict); - Mock mockRunner = CreateMockRunner(response); - - ParseUserController controller = new ParseUserController(mockRunner.Object); - return controller.LogInAsync("facebook", data: null, cancellationToken: CancellationToken.None).ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Uri.AbsolutePath == "/1/users"), - It.IsAny>(), - It.IsAny>(), - It.IsAny()), Times.Exactly(1)); - - IObjectState newState = t.Result; - Assert.AreEqual("s3ss10nt0k3n", newState["sessionToken"]); - Assert.AreEqual("d3ImSh3ki", newState.ObjectId); - Assert.IsNotNull(newState.CreatedAt); - Assert.IsNotNull(newState.UpdatedAt); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserControllerTests))] - public Task TestGetUserFromSessionToken() - { - Dictionary responseDict = new Dictionary() { - { "__type", "Object" }, - { "className", "_User" }, - { "objectId", "d3ImSh3ki" }, - { "sessionToken", "s3ss10nt0k3n" }, - { "createdAt", "2015-09-18T18:11:28.943Z" } - }; - Tuple> response = new Tuple>(HttpStatusCode.Accepted, responseDict); - Mock mockRunner = CreateMockRunner(response); - - ParseUserController controller = new ParseUserController(mockRunner.Object); - return controller.GetUserAsync("s3ss10nt0k3n", CancellationToken.None).ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Uri.AbsolutePath == "/1/users/me"), - It.IsAny>(), - It.IsAny>(), - It.IsAny()), Times.Exactly(1)); - - IObjectState newState = t.Result; - Assert.AreEqual("s3ss10nt0k3n", newState["sessionToken"]); - Assert.AreEqual("d3ImSh3ki", newState.ObjectId); - Assert.IsNotNull(newState.CreatedAt); - Assert.IsNotNull(newState.UpdatedAt); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserControllerTests))] - public Task TestRequestPasswordReset() - { - Dictionary responseDict = new Dictionary(); - Tuple> response = new Tuple>(HttpStatusCode.Accepted, responseDict); - Mock mockRunner = CreateMockRunner(response); - - ParseUserController controller = new ParseUserController(mockRunner.Object); - return controller.RequestPasswordResetAsync("gogo@parse.com", CancellationToken.None).ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Uri.AbsolutePath == "/1/requestPasswordReset"), - It.IsAny>(), - It.IsAny>(), - It.IsAny()), Times.Exactly(1)); - }); - } - - private Mock CreateMockRunner(Tuple> response) - { - Mock mockRunner = new Mock(); - mockRunner.Setup(obj => obj.RunCommandAsync(It.IsAny(), - It.IsAny>(), - It.IsAny>(), - It.IsAny())) - .Returns(Task>>.FromResult(response)); - - return mockRunner; - } - } -} diff --git a/Parse.Test/UserTests.cs b/Parse.Test/UserTests.cs deleted file mode 100644 index 01b98caa..00000000 --- a/Parse.Test/UserTests.cs +++ /dev/null @@ -1,771 +0,0 @@ -using Moq; -using Parse; -using Parse.Core.Internal; -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Parse.Test -{ - [TestClass] - public class UserTests - { - [TestInitialize] - public void SetUp() - { - ParseObject.RegisterSubclass(); - ParseObject.RegisterSubclass(); - } - - [TestCleanup] - public void TearDown() => ParseCorePlugins.Instance = null; - - [TestMethod] - public void TestRemoveFields() - { - IObjectState state = new MutableObjectState - { - ServerData = new Dictionary() { - { "username", "kevin" }, - { "name", "andrew" } - } - }; - ParseUser user = ParseObjectExtensions.FromState(state, "_User"); - Assert.ThrowsException(() => user.Remove("username")); - try - { user.Remove("name"); } - catch { Assert.Fail(); } - Assert.IsFalse(user.ContainsKey("name")); - } - - [TestMethod] - public void TestSessionTokenGetter() - { - IObjectState state = new MutableObjectState - { - ServerData = new Dictionary() { - { "username", "kevin" }, - { "sessionToken", "se551onT0k3n" } - } - }; - ParseUser user = ParseObjectExtensions.FromState(state, "_User"); - Assert.AreEqual("se551onT0k3n", user.SessionToken); - } - - [TestMethod] - public void TestUsernameGetterSetter() - { - IObjectState state = new MutableObjectState - { - ServerData = new Dictionary() { - { "username", "kevin" }, - } - }; - ParseUser user = ParseObjectExtensions.FromState(state, "_User"); - Assert.AreEqual("kevin", user.Username); - user.Username = "ilya"; - Assert.AreEqual("ilya", user.Username); - } - - [TestMethod] - public void TestPasswordGetterSetter() - { - IObjectState state = new MutableObjectState - { - ServerData = new Dictionary() { - { "username", "kevin" }, - { "password", "hurrah" }, - } - }; - ParseUser user = ParseObjectExtensions.FromState(state, "_User"); - Assert.AreEqual("hurrah", user.GetState()["password"]); - user.Password = "david"; - Assert.IsNotNull(user.GetCurrentOperations()["password"]); - } - - [TestMethod] - public void TestEmailGetterSetter() - { - IObjectState state = new MutableObjectState - { - ServerData = new Dictionary() { - { "email", "james@parse.com" }, - { "name", "andrew" }, - { "sessionToken", "se551onT0k3n" } - } - }; - ParseUser user = ParseObjectExtensions.FromState(state, "_User"); - Assert.AreEqual("james@parse.com", user.Email); - user.Email = "bryan@parse.com"; - Assert.AreEqual("bryan@parse.com", user.Email); - } - - [TestMethod] - public void TestAuthDataGetter() - { - IObjectState state = new MutableObjectState - { - ServerData = new Dictionary() { - { "email", "james@parse.com" }, - { "authData", new Dictionary() { - { "facebook", new Dictionary() { - { "sessionToken", "none" } - }} - }} - } - }; - ParseUser user = ParseObjectExtensions.FromState(state, "_User"); - Assert.AreEqual(1, user.GetAuthData().Count); - Assert.IsInstanceOfType(user.GetAuthData()["facebook"], typeof(IDictionary)); - } - - [TestMethod] - public void TestGetUserQuery() => Assert.IsInstanceOfType(ParseUser.Query, typeof(ParseQuery)); - - [TestMethod] - public void TestIsAuthenticated() - { - IObjectState state = new MutableObjectState - { - ObjectId = "wagimanPutraPetir", - ServerData = new Dictionary() { - { "sessionToken", "llaKcolnu" } - } - }; - ParseUser user = ParseObjectExtensions.FromState(state, "_User"); - Mock mockCurrentUserController = new Mock(); - mockCurrentUserController.Setup(obj => obj.GetAsync(It.IsAny())) - .Returns(Task.FromResult(user)); - ParseCorePlugins.Instance = new ParseCorePlugins - { - CurrentUserController = mockCurrentUserController.Object - }; - ParseObject.RegisterSubclass(); - ParseObject.RegisterSubclass(); - - Assert.IsTrue(user.IsAuthenticated); - } - - [TestMethod] - public void TestIsAuthenticatedWithOtherParseUser() - { - IObjectState state = new MutableObjectState - { - ObjectId = "wagimanPutraPetir", - ServerData = new Dictionary() { - { "sessionToken", "llaKcolnu" } - } - }; - IObjectState state2 = new MutableObjectState - { - ObjectId = "wagimanPutraPetir2", - ServerData = new Dictionary() { - { "sessionToken", "llaKcolnu" } - } - }; - ParseUser user = ParseObjectExtensions.FromState(state, "_User"); - ParseUser user2 = ParseObjectExtensions.FromState(state2, "_User"); - Mock mockCurrentUserController = new Mock(); - mockCurrentUserController.Setup(obj => obj.GetAsync(It.IsAny())) - .Returns(Task.FromResult(user)); - ParseCorePlugins.Instance = new ParseCorePlugins - { - CurrentUserController = mockCurrentUserController.Object - }; - ParseObject.RegisterSubclass(); - ParseObject.RegisterSubclass(); - - Assert.IsFalse(user2.IsAuthenticated); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserTests))] - public Task TestSignUpWithInvalidServerData() - { - IObjectState state = new MutableObjectState - { - ServerData = new Dictionary() { - { "sessionToken", "llaKcolnu" } - } - }; - ParseUser user = ParseObjectExtensions.FromState(state, "_User"); - - return user.SignUpAsync().ContinueWith(t => - { - Assert.IsTrue(t.IsFaulted); - Assert.IsInstanceOfType(t.Exception.InnerException, typeof(InvalidOperationException)); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserTests))] - public Task TestSignUp() - { - IObjectState state = new MutableObjectState - { - ServerData = new Dictionary() { - { "sessionToken", "llaKcolnu" }, - { "username", "ihave" }, - { "password", "adream" } - } - }; - IObjectState newState = new MutableObjectState - { - ObjectId = "some0neTol4v4" - }; - ParseUser user = ParseObjectExtensions.FromState(state, "_User"); - Mock mockController = new Mock(); - mockController.Setup(obj => obj.SignUpAsync(It.IsAny(), - It.IsAny>(), - It.IsAny())).Returns(Task.FromResult(newState)); - ParseCorePlugins.Instance = new ParseCorePlugins - { - UserController = mockController.Object - }; - ParseObject.RegisterSubclass(); - ParseObject.RegisterSubclass(); - - return user.SignUpAsync().ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - mockController.Verify(obj => obj.SignUpAsync(It.IsAny(), - It.IsAny>(), - It.IsAny()), Times.Exactly(1)); - Assert.IsFalse(user.IsDirty); - Assert.AreEqual("ihave", user.Username); - Assert.IsFalse(user.GetState().ContainsKey("password")); - Assert.AreEqual("some0neTol4v4", user.ObjectId); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserTests))] - public Task TestLogIn() - { - IObjectState state = new MutableObjectState - { - ServerData = new Dictionary() { - { "sessionToken", "llaKcolnu" }, - { "username", "ihave" }, - { "password", "adream" } - } - }; - IObjectState newState = new MutableObjectState - { - ObjectId = "some0neTol4v4" - }; - Mock mockController = new Mock(); - mockController.Setup(obj => obj.LogInAsync("ihave", - "adream", - It.IsAny())).Returns(Task.FromResult(newState)); - ParseCorePlugins.Instance = new ParseCorePlugins - { - UserController = mockController.Object - }; - ParseObject.RegisterSubclass(); - ParseObject.RegisterSubclass(); - - return ParseUser.LogInAsync("ihave", "adream").ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - mockController.Verify(obj => obj.LogInAsync("ihave", - "adream", - It.IsAny()), Times.Exactly(1)); - - ParseUser user = t.Result; - Assert.IsFalse(user.IsDirty); - Assert.IsNull(user.Username); - Assert.AreEqual("some0neTol4v4", user.ObjectId); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserTests))] - public Task TestBecome() - { - IObjectState state = new MutableObjectState - { - ObjectId = "some0neTol4v4", - ServerData = new Dictionary { ["sessionToken"] = "llaKcolnu" } - }; - Mock mockController = new Mock(); - mockController.Setup(obj => obj.GetUserAsync("llaKcolnu", It.IsAny())).Returns(Task.FromResult(state)); - ParseCorePlugins.Instance = new ParseCorePlugins - { - UserController = mockController.Object - }; - ParseObject.RegisterSubclass(); - ParseObject.RegisterSubclass(); - - return ParseUser.BecomeAsync("llaKcolnu").ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - mockController.Verify(obj => obj.GetUserAsync("llaKcolnu", It.IsAny()), Times.Exactly(1)); - - ParseUser user = t.Result; - Assert.AreEqual("some0neTol4v4", user.ObjectId); - Assert.AreEqual("llaKcolnu", user.SessionToken); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserTests))] - public Task TestLogOut() - { - IObjectState state = new MutableObjectState - { - ServerData = new Dictionary() { - { "sessionToken", "r:llaKcolnu" } - } - }; - ParseUser user = ParseObjectExtensions.FromState(state, "_User"); - Mock mockCurrentUserController = new Mock(); - mockCurrentUserController.Setup(obj => obj.GetAsync(It.IsAny())) - .Returns(Task.FromResult(user)); - Mock mockSessionController = new Mock(); - mockSessionController.Setup(c => c.IsRevocableSessionToken(It.IsAny())).Returns(true); - - ParseCorePlugins.Instance = new ParseCorePlugins - { - CurrentUserController = mockCurrentUserController.Object, - SessionController = mockSessionController.Object - }; - ParseObject.RegisterSubclass(); - ParseObject.RegisterSubclass(); - - return ParseUser.LogOutAsync().ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - mockCurrentUserController.Verify(obj => obj.LogOutAsync(It.IsAny()), Times.Exactly(1)); - mockSessionController.Verify(obj => obj.RevokeAsync("r:llaKcolnu", It.IsAny()), Times.Exactly(1)); - }); - } - - [TestMethod] - public void TestCurrentUser() - { - IObjectState state = new MutableObjectState - { - ServerData = new Dictionary() { - { "sessionToken", "llaKcolnu" } - } - }; - ParseUser user = ParseObjectExtensions.FromState(state, "_User"); - Mock mockCurrentUserController = new Mock(); - mockCurrentUserController.Setup(obj => obj.GetAsync(It.IsAny())) - .Returns(Task.FromResult(user)); - ParseCorePlugins.Instance = new ParseCorePlugins - { - CurrentUserController = mockCurrentUserController.Object, - }; - ParseObject.RegisterSubclass(); - ParseObject.RegisterSubclass(); - - Assert.AreEqual(user, ParseUser.CurrentUser); - } - - [TestMethod] - public void TestCurrentUserWithEmptyResult() - { - Mock mockCurrentUserController = new Mock(); - ParseCorePlugins.Instance = new ParseCorePlugins - { - CurrentUserController = mockCurrentUserController.Object - }; - ParseObject.RegisterSubclass(); - ParseObject.RegisterSubclass(); - - Assert.IsNull(ParseUser.CurrentUser); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserTests))] - public Task TestRevocableSession() - { - IObjectState state = new MutableObjectState - { - ServerData = new Dictionary() { - { "sessionToken", "llaKcolnu" } - } - }; - IObjectState newState = new MutableObjectState - { - ServerData = new Dictionary() { - { "sessionToken", "r:llaKcolnu" } - } - }; - ParseUser user = ParseObjectExtensions.FromState(state, "_User"); - Mock mockSessionController = new Mock(); - mockSessionController.Setup(obj => obj.UpgradeToRevocableSessionAsync("llaKcolnu", - It.IsAny())).Returns(Task.FromResult(newState)); - ParseCorePlugins.Instance = new ParseCorePlugins - { - SessionController = mockSessionController.Object - }; - ParseObject.RegisterSubclass(); - ParseObject.RegisterSubclass(); - - return user.UpgradeToRevocableSessionAsync(CancellationToken.None).ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - mockSessionController.Verify(obj => obj.UpgradeToRevocableSessionAsync("llaKcolnu", - It.IsAny()), Times.Exactly(1)); - Assert.AreEqual("r:llaKcolnu", user.SessionToken); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserTests))] - public Task TestRequestPasswordReset() - { - Mock mockController = new Mock(); - ParseCorePlugins.Instance = new ParseCorePlugins - { - UserController = mockController.Object - }; - ParseObject.RegisterSubclass(); - ParseObject.RegisterSubclass(); - - return ParseUser.RequestPasswordResetAsync("gogo@parse.com").ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - mockController.Verify(obj => obj.RequestPasswordResetAsync("gogo@parse.com", - It.IsAny()), Times.Exactly(1)); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserTests))] - public Task TestUserSave() - { - IObjectState state = new MutableObjectState - { - ObjectId = "some0neTol4v4", - ServerData = new Dictionary() { - { "sessionToken", "llaKcolnu" }, - { "username", "ihave" }, - { "password", "adream" } - } - }; - IObjectState newState = new MutableObjectState - { - ServerData = new Dictionary() { - { "Alliance", "rekt" } - } - }; - ParseUser user = ParseObjectExtensions.FromState(state, "_User"); - Mock mockObjectController = new Mock(); - mockObjectController.Setup(obj => obj.SaveAsync(It.IsAny(), - It.IsAny>(), - It.IsAny(), - It.IsAny())).Returns(Task.FromResult(newState)); - ParseCorePlugins.Instance = new ParseCorePlugins - { - ObjectController = mockObjectController.Object, - CurrentUserController = new Mock().Object - }; - ParseObject.RegisterSubclass(); - ParseObject.RegisterSubclass(); - user["Alliance"] = "rekt"; - - return user.SaveAsync().ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - mockObjectController.Verify(obj => obj.SaveAsync(It.IsAny(), - It.IsAny>(), - It.IsAny(), - It.IsAny()), Times.Exactly(1)); - Assert.IsFalse(user.IsDirty); - Assert.AreEqual("ihave", user.Username); - Assert.IsFalse(user.GetState().ContainsKey("password")); - Assert.AreEqual("some0neTol4v4", user.ObjectId); - Assert.AreEqual("rekt", user["Alliance"]); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserTests))] - public Task TestUserFetch() - { - IObjectState state = new MutableObjectState - { - ObjectId = "some0neTol4v4", - ServerData = new Dictionary() { - { "sessionToken", "llaKcolnu" }, - { "username", "ihave" }, - { "password", "adream" } - } - }; - IObjectState newState = new MutableObjectState - { - ServerData = new Dictionary() { - { "Alliance", "rekt" } - } - }; - ParseUser user = ParseObjectExtensions.FromState(state, "_User"); - Mock mockObjectController = new Mock(); - mockObjectController.Setup(obj => obj.FetchAsync(It.IsAny(), - It.IsAny(), - It.IsAny())).Returns(Task.FromResult(newState)); - ParseCorePlugins.Instance = new ParseCorePlugins - { - ObjectController = mockObjectController.Object, - CurrentUserController = new Mock().Object - }; - ParseObject.RegisterSubclass(); - ParseObject.RegisterSubclass(); - user["Alliance"] = "rekt"; - - return user.FetchAsync().ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - mockObjectController.Verify(obj => obj.FetchAsync(It.IsAny(), - It.IsAny(), - It.IsAny()), Times.Exactly(1)); - Assert.IsTrue(user.IsDirty); - Assert.AreEqual("ihave", user.Username); - Assert.IsTrue(user.GetState().ContainsKey("password")); - Assert.AreEqual("some0neTol4v4", user.ObjectId); - Assert.AreEqual("rekt", user["Alliance"]); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserTests))] - public Task TestLink() - { - IObjectState state = new MutableObjectState - { - ObjectId = "some0neTol4v4", - ServerData = new Dictionary() { - { "sessionToken", "llaKcolnu" } - } - }; - IObjectState newState = new MutableObjectState - { - ServerData = new Dictionary() { - { "garden", "ofWords" } - } - }; - ParseUser user = ParseObjectExtensions.FromState(state, "_User"); - Mock mockObjectController = new Mock(); - mockObjectController.Setup(obj => obj.SaveAsync(It.IsAny(), - It.IsAny>(), - It.IsAny(), - It.IsAny())).Returns(Task.FromResult(newState)); - ParseCorePlugins.Instance = new ParseCorePlugins - { - ObjectController = mockObjectController.Object, - CurrentUserController = new Mock().Object - }; - ParseObject.RegisterSubclass(); - ParseObject.RegisterSubclass(); - - return user.LinkWithAsync("parse", new Dictionary(), CancellationToken.None).ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - mockObjectController.Verify(obj => obj.SaveAsync(It.IsAny(), - It.IsAny>(), - It.IsAny(), - It.IsAny()), Times.Exactly(1)); - Assert.IsFalse(user.IsDirty); - Assert.IsNotNull(user.GetAuthData()); - Assert.IsNotNull(user.GetAuthData()["parse"]); - Assert.AreEqual("some0neTol4v4", user.ObjectId); - Assert.AreEqual("ofWords", user["garden"]); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserTests))] - public Task TestUnlink() - { - IObjectState state = new MutableObjectState - { - ObjectId = "some0neTol4v4", - ServerData = new Dictionary() { - { "sessionToken", "llaKcolnu" }, - { "authData", new Dictionary { - { "parse", new Dictionary() } - }} - } - }; - IObjectState newState = new MutableObjectState - { - ServerData = new Dictionary() { - { "garden", "ofWords" } - } - }; - ParseUser user = ParseObjectExtensions.FromState(state, "_User"); - Mock mockObjectController = new Mock(); - mockObjectController.Setup(obj => obj.SaveAsync(It.IsAny(), - It.IsAny>(), - It.IsAny(), - It.IsAny())).Returns(Task.FromResult(newState)); - Mock mockCurrentUserController = new Mock(); - mockCurrentUserController.Setup(obj => obj.IsCurrent(user)).Returns(true); - ParseCorePlugins.Instance = new ParseCorePlugins - { - ObjectController = mockObjectController.Object, - CurrentUserController = mockCurrentUserController.Object, - }; - ParseObject.RegisterSubclass(); - ParseObject.RegisterSubclass(); - - return user.UnlinkFromAsync("parse", CancellationToken.None).ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - mockObjectController.Verify(obj => obj.SaveAsync(It.IsAny(), - It.IsAny>(), - It.IsAny(), - It.IsAny()), Times.Exactly(1)); - Assert.IsFalse(user.IsDirty); - Assert.IsNotNull(user.GetAuthData()); - Assert.IsFalse(user.GetAuthData().ContainsKey("parse")); - Assert.AreEqual("some0neTol4v4", user.ObjectId); - Assert.AreEqual("ofWords", user["garden"]); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserTests))] - public Task TestUnlinkNonCurrentUser() - { - IObjectState state = new MutableObjectState - { - ObjectId = "some0neTol4v4", - ServerData = new Dictionary() { - { "sessionToken", "llaKcolnu" }, - { "authData", new Dictionary { - { "parse", new Dictionary() } - }} - } - }; - IObjectState newState = new MutableObjectState - { - ServerData = new Dictionary() { - { "garden", "ofWords" } - } - }; - ParseUser user = ParseObjectExtensions.FromState(state, "_User"); - Mock mockObjectController = new Mock(); - mockObjectController.Setup(obj => obj.SaveAsync(It.IsAny(), - It.IsAny>(), - It.IsAny(), - It.IsAny())).Returns(Task.FromResult(newState)); - Mock mockCurrentUserController = new Mock(); - mockCurrentUserController.Setup(obj => obj.IsCurrent(user)).Returns(false); - ParseCorePlugins.Instance = new ParseCorePlugins - { - ObjectController = mockObjectController.Object, - CurrentUserController = mockCurrentUserController.Object, - }; - ParseObject.RegisterSubclass(); - ParseObject.RegisterSubclass(); - - return user.UnlinkFromAsync("parse", CancellationToken.None).ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - mockObjectController.Verify(obj => obj.SaveAsync(It.IsAny(), - It.IsAny>(), - It.IsAny(), - It.IsAny()), Times.Exactly(1)); - Assert.IsFalse(user.IsDirty); - Assert.IsNotNull(user.GetAuthData()); - Assert.IsTrue(user.GetAuthData().ContainsKey("parse")); - Assert.IsNull(user.GetAuthData()["parse"]); - Assert.AreEqual("some0neTol4v4", user.ObjectId); - Assert.AreEqual("ofWords", user["garden"]); - }); - } - - [TestMethod] - [AsyncStateMachine(typeof(UserTests))] - public Task TestLogInWith() - { - IObjectState state = new MutableObjectState - { - ObjectId = "some0neTol4v4", - ServerData = new Dictionary() { - { "sessionToken", "llaKcolnu" } - } - }; - Mock mockController = new Mock(); - mockController.Setup(obj => obj.LogInAsync("parse", - It.IsAny>(), - It.IsAny())).Returns(Task.FromResult(state)); - - ParseCorePlugins.Instance = new ParseCorePlugins - { - UserController = mockController.Object - }; - ParseObject.RegisterSubclass(); - ParseObject.RegisterSubclass(); - - return ParseUserExtensions.LogInWithAsync("parse", new Dictionary(), CancellationToken.None).ContinueWith(t => - { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - mockController.Verify(obj => obj.LogInAsync("parse", - It.IsAny>(), - It.IsAny()), Times.Exactly(1)); - - ParseUser user = t.Result; - Assert.IsNotNull(user.GetAuthData()); - Assert.IsNotNull(user.GetAuthData()["parse"]); - Assert.AreEqual("some0neTol4v4", user.ObjectId); - }); - } - - [TestMethod] - public void TestImmutableKeys() - { - ParseUser user = new ParseUser(); - string[] immutableKeys = new string[] { - "sessionToken", "isNew" - }; - - foreach (string key in immutableKeys) - { - Assert.ThrowsException(() => - user[key] = "1234567890" - ); - - Assert.ThrowsException(() => - user.Add(key, "1234567890") - ); - - Assert.ThrowsException(() => - user.AddRangeUniqueToList(key, new string[] { "1234567890" }) - ); - - Assert.ThrowsException(() => - user.Remove(key) - ); - - Assert.ThrowsException(() => - user.RemoveAllFromList(key, new string[] { "1234567890" }) - ); - } - - // Other special keys should be good - user["username"] = "username"; - user["password"] = "password"; - } - } -} diff --git a/Parse.Test/ACLTests.cs b/Parse.Tests/ACLTests.cs similarity index 74% rename from Parse.Test/ACLTests.cs rename to Parse.Tests/ACLTests.cs index 0524e558..b6e01fec 100644 --- a/Parse.Test/ACLTests.cs +++ b/Parse.Tests/ACLTests.cs @@ -1,26 +1,24 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using Parse.Core.Internal; using System; -using System.Collections.Generic; -using System.Text; -using System.Threading; -using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Parse.Infrastructure; +using Parse.Platform.Objects; -namespace Parse.Test +namespace Parse.Tests { [TestClass] public class ACLTests { + ParseClient Client { get; set; } = new ParseClient(new ServerConnectionData { Test = true }); + [TestInitialize] - public void SetUp() + public void Initialize() { - ParseObject.RegisterSubclass(); - ParseObject.RegisterSubclass(); + Client.AddValidClass(); + Client.AddValidClass(); } [TestCleanup] - public void TearDown() => ParseCorePlugins.Instance = null; + public void Clean() => (Client.Services as ServiceHub).Reset(); [TestMethod] public void TestCheckPermissionsWithParseUserConstructor() @@ -53,8 +51,8 @@ public void TestReadWriteMutationWithParseUserConstructor() } [TestMethod] - public void TestParseACLCreationWithNullObjectIdParseUser() => Assert.ThrowsException(() => new ParseACL(GenerateUser(null))); + public void TestParseACLCreationWithNullObjectIdParseUser() => Assert.ThrowsException(() => new ParseACL(GenerateUser(default))); - ParseUser GenerateUser(string objectID) => ParseObjectExtensions.FromState(new MutableObjectState { ObjectId = objectID }, "_User"); + ParseUser GenerateUser(string objectID) => Client.GenerateObjectFromState(new MutableObjectState { ObjectId = objectID }, "_User"); } } diff --git a/Parse.Test/AnalyticsControllerTests.cs b/Parse.Tests/AnalyticsControllerTests.cs similarity index 51% rename from Parse.Test/AnalyticsControllerTests.cs rename to Parse.Tests/AnalyticsControllerTests.cs index f92a6f96..07a63b77 100644 --- a/Parse.Test/AnalyticsControllerTests.cs +++ b/Parse.Tests/AnalyticsControllerTests.cs @@ -1,23 +1,26 @@ -using Moq; -using Parse; -using Parse.Analytics.Internal; -using Parse.Core.Internal; using System; using System.Collections.Generic; using System.Net; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Parse.Infrastructure; +using Parse.Abstractions.Infrastructure.Execution; +using Parse.Abstractions.Infrastructure; +using Parse.Platform.Analytics; +using Parse.Infrastructure.Execution; -namespace Parse.Test +namespace Parse.Tests { [TestClass] public class AnalyticsControllerTests { + ParseClient Client { get; set; } + [TestInitialize] - public void SetUp() => ParseClient.Initialize(new ParseClient.Configuration { ApplicationID = "", Key = "" }); + public void SetUp() => Client = new ParseClient(new ServerConnectionData { ApplicationID = "", Key = "", Test = true }); [TestMethod] [AsyncStateMachine(typeof(AnalyticsControllerTests))] @@ -25,11 +28,12 @@ public Task TestTrackEventWithEmptyDimensions() { Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, new Dictionary { })); - return new ParseAnalyticsController(mockRunner.Object).TrackEventAsync("SomeEvent", dimensions: null, sessionToken: null, cancellationToken: CancellationToken.None).ContinueWith(t => + return new ParseAnalyticsController(mockRunner.Object).TrackEventAsync("SomeEvent", dimensions: default, sessionToken: default, serviceHub: Client, cancellationToken: CancellationToken.None).ContinueWith(task => { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Uri.AbsolutePath == "/1/events/SomeEvent"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + + mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "events/SomeEvent"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); }); } @@ -39,13 +43,12 @@ public Task TestTrackEventWithNonEmptyDimensions() { Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, new Dictionary { })); - Dictionary dimensions = new Dictionary { ["njwerjk12"] = "5523dd" }; - - return new ParseAnalyticsController(mockRunner.Object).TrackEventAsync("SomeEvent", dimensions: dimensions, sessionToken: null, cancellationToken: CancellationToken.None).ContinueWith(t => + return new ParseAnalyticsController(mockRunner.Object).TrackEventAsync("SomeEvent", dimensions: new Dictionary { ["njwerjk12"] = "5523dd" }, sessionToken: default, serviceHub: Client, cancellationToken: CancellationToken.None).ContinueWith(task => { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Uri.AbsolutePath.Contains("/1/events/SomeEvent")), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + + mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path.Contains("events/SomeEvent")), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); }); } @@ -53,13 +56,14 @@ public Task TestTrackEventWithNonEmptyDimensions() [AsyncStateMachine(typeof(AnalyticsControllerTests))] public Task TestTrackAppOpenedWithEmptyPushHash() { - Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, new Dictionary())); + Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, new Dictionary { })); - return new ParseAnalyticsController(mockRunner.Object).TrackAppOpenedAsync(null, sessionToken: null, cancellationToken: CancellationToken.None).ContinueWith(t => + return new ParseAnalyticsController(mockRunner.Object).TrackAppOpenedAsync(default, sessionToken: default, serviceHub: Client, cancellationToken: CancellationToken.None).ContinueWith(task => { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Uri.AbsolutePath == "/1/events/AppOpened"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + + mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "events/AppOpened"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); }); } @@ -69,20 +73,19 @@ public Task TestTrackAppOpenedWithNonEmptyPushHash() { Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, new Dictionary())); - string pushHash = "32j4hll12lkk"; - - return new ParseAnalyticsController(mockRunner.Object).TrackAppOpenedAsync(pushHash, sessionToken: null, cancellationToken: CancellationToken.None).ContinueWith(t => + return new ParseAnalyticsController(mockRunner.Object).TrackAppOpenedAsync("32j4hll12lkk", sessionToken: default, serviceHub: Client, cancellationToken: CancellationToken.None).ContinueWith(task => { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); - mockRunner.Verify(obj => obj.RunCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + + mockRunner.Verify(obj => obj.RunCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); }); } Mock CreateMockRunner(Tuple> response) { Mock mockRunner = new Mock(); - mockRunner.Setup(obj => obj.RunCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(response)); + mockRunner.Setup(obj => obj.RunCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(response)); return mockRunner; } diff --git a/Parse.Tests/AnalyticsTests.cs b/Parse.Tests/AnalyticsTests.cs new file mode 100644 index 00000000..c5efee08 --- /dev/null +++ b/Parse.Tests/AnalyticsTests.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Parse.Abstractions.Infrastructure; +using Parse.Infrastructure; +using Parse.Abstractions.Platform.Analytics; +using Parse.Abstractions.Platform.Users; + +namespace Parse.Tests +{ + [TestClass] + public class AnalyticsTests + { +#warning Skipped post-test-evaluation cleaning method may be needed. + + // [TestCleanup] + // public void TearDown() => (Client.Services as ServiceHub).Reset(); + + [TestMethod] + [AsyncStateMachine(typeof(AnalyticsTests))] + public Task TestTrackEvent() + { + MutableServiceHub hub = new MutableServiceHub { }; + ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + Mock mockController = new Mock { }; + Mock mockCurrentUserController = new Mock { }; + + mockCurrentUserController.Setup(controller => controller.GetCurrentSessionTokenAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult("sessionToken")); + + hub.AnalyticsController = mockController.Object; + hub.CurrentUserController = mockCurrentUserController.Object; + + return client.TrackAnalyticsEventAsync("SomeEvent").ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + + mockController.Verify(obj => obj.TrackEventAsync(It.Is(eventName => eventName == "SomeEvent"), It.Is>(dict => dict == null), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(AnalyticsTests))] + public Task TestTrackEventWithDimension() + { + MutableServiceHub hub = new MutableServiceHub { }; + ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + Mock mockController = new Mock { }; + Mock mockCurrentUserController = new Mock { }; + + mockCurrentUserController.Setup(controller => controller.GetCurrentSessionTokenAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult("sessionToken")); + + hub.AnalyticsController = mockController.Object; + hub.CurrentUserController = mockCurrentUserController.Object; + + return client.TrackAnalyticsEventAsync("SomeEvent", new Dictionary { ["facebook"] = "hq" }).ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + mockController.Verify(obj => obj.TrackEventAsync(It.Is(eventName => eventName == "SomeEvent"), It.Is>(dict => dict != null && dict.Count == 1), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(AnalyticsTests))] + public Task TestTrackAppOpened() + { + MutableServiceHub hub = new MutableServiceHub { }; + ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + Mock mockController = new Mock { }; + Mock mockCurrentUserController = new Mock { }; + + mockCurrentUserController.Setup(controller => controller.GetCurrentSessionTokenAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult("sessionToken")); + + hub.AnalyticsController = mockController.Object; + hub.CurrentUserController = mockCurrentUserController.Object; + + return client.TrackLaunchAsync().ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + + mockController.Verify(obj => obj.TrackAppOpenedAsync(It.Is(pushHash => pushHash == null), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); + }); + } + } +} diff --git a/Parse.Tests/CloudControllerTests.cs b/Parse.Tests/CloudControllerTests.cs new file mode 100644 index 00000000..81de1986 --- /dev/null +++ b/Parse.Tests/CloudControllerTests.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Execution; +using Parse.Infrastructure; +using Parse.Infrastructure.Execution; +using Parse.Platform.Cloud; + +namespace Parse.Tests +{ +#warning Class refactoring requires completion. + + [TestClass] + public class CloudControllerTests + { + ParseClient Client { get; set; } + + [TestInitialize] + public void SetUp() => Client = new ParseClient(new ServerConnectionData { ApplicationID = "", Key = "", Test = true }); + + [TestMethod] + [AsyncStateMachine(typeof(CloudControllerTests))] + public Task TestEmptyCallFunction() => new ParseCloudCodeController(CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, default)).Object, Client.Decoder).CallFunctionAsync("someFunction", default, default, Client, CancellationToken.None).ContinueWith(task => + { + Assert.IsTrue(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + }); + + [TestMethod] + [AsyncStateMachine(typeof(CloudControllerTests))] + public Task TestCallFunction() + { + Dictionary responseDict = new Dictionary { ["result"] = "gogo" }; + Tuple> response = new Tuple>(HttpStatusCode.Accepted, responseDict); + Mock mockRunner = CreateMockRunner(response); + + return new ParseCloudCodeController(mockRunner.Object, Client.Decoder).CallFunctionAsync("someFunction", default, default, Client, CancellationToken.None).ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + Assert.AreEqual("gogo", task.Result); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(CloudControllerTests))] + public Task TestCallFunctionWithComplexType() => new ParseCloudCodeController(CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, new Dictionary { { "result", new Dictionary { { "fosco", "ben" }, { "list", new List { 1, 2, 3 } } } } })).Object, Client.Decoder).CallFunctionAsync>("someFunction", default, default, Client, CancellationToken.None).ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + Assert.IsInstanceOfType(task.Result, typeof(IDictionary)); + Assert.AreEqual("ben", task.Result["fosco"]); + Assert.IsInstanceOfType(task.Result["list"], typeof(IList)); + }); + + [TestMethod] + [AsyncStateMachine(typeof(CloudControllerTests))] + public Task TestCallFunctionWithWrongType() => new ParseCloudCodeController(CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, new Dictionary() { { "result", "gogo" } })).Object, Client.Decoder).CallFunctionAsync("someFunction", default, default, Client, CancellationToken.None).ContinueWith(task => + { + Assert.IsTrue(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + }); + + private Mock CreateMockRunner(Tuple> response) + { + Mock mockRunner = new Mock { }; + mockRunner.Setup(obj => obj.RunCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(response)); + + return mockRunner; + } + } +} diff --git a/Parse.Tests/CloudTests.cs b/Parse.Tests/CloudTests.cs new file mode 100644 index 00000000..dd1a6878 --- /dev/null +++ b/Parse.Tests/CloudTests.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Platform.Cloud; +using Parse.Abstractions.Platform.Users; +using Parse.Infrastructure; + +namespace Parse.Tests +{ + [TestClass] + public class CloudTests + { +#warning Skipped post-test-evaluation cleaning method may be needed. + + // [TestCleanup] + // public void TearDown() => ParseCorePlugins.Instance.Reset(); + + [TestMethod] + [AsyncStateMachine(typeof(CloudTests))] + public Task TestCloudFunctions() + { + MutableServiceHub hub = new MutableServiceHub { }; + ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + Mock mockController = new Mock(); + mockController.Setup(obj => obj.CallFunctionAsync>(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult>(new Dictionary { ["fosco"] = "ben", ["list"] = new List { 1, 2, 3 } })); + + hub.CloudCodeController = mockController.Object; + hub.CurrentUserController = new Mock { }.Object; + + return client.CallCloudCodeFunctionAsync>("someFunction", null, CancellationToken.None).ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + Assert.IsInstanceOfType(task.Result, typeof(IDictionary)); + Assert.AreEqual("ben", task.Result["fosco"]); + Assert.IsInstanceOfType(task.Result["list"], typeof(IList)); + }); + } + } +} diff --git a/Parse.Tests/CommandTests.cs b/Parse.Tests/CommandTests.cs new file mode 100644 index 00000000..8091b75c --- /dev/null +++ b/Parse.Tests/CommandTests.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Parse.Infrastructure; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Platform.Installations; +using Parse.Abstractions.Platform.Users; +using Parse.Infrastructure.Execution; +using Parse.Abstractions.Infrastructure.Execution; + +namespace Parse.Tests +{ +#warning Initialization and cleaning steps may be redundant for each test method. It may be possible to simply reset the required services before each run. +#warning Class refactoring requires completion. + + [TestClass] + public class CommandTests + { + ParseClient Client { get; set; } + + [TestInitialize] + public void Initialize() => Client = new ParseClient(new ServerConnectionData { ApplicationID = "", Key = "", Test = true }); + + [TestCleanup] + public void Clean() => (Client.Services as ServiceHub).Reset(); + + [TestMethod] + public void TestMakeCommand() + { + ParseCommand command = new ParseCommand("endpoint", method: "GET", sessionToken: "abcd", headers: default, data: default); + + Assert.AreEqual("endpoint", command.Path); + Assert.AreEqual("GET", command.Method); + Assert.IsTrue(command.Headers.Any(pair => pair.Key == "X-Parse-Session-Token" && pair.Value == "abcd")); + } + + [TestMethod] + [AsyncStateMachine(typeof(CommandTests))] + public Task TestRunCommand() + { + Mock mockHttpClient = new Mock(); + Mock mockInstallationController = new Mock(); + Task> fakeResponse = Task.FromResult(new Tuple(HttpStatusCode.OK, "{}")); + mockHttpClient.Setup(obj => obj.ExecuteAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(fakeResponse); + + mockInstallationController.Setup(installation => installation.GetAsync()).Returns(Task.FromResult(default)); + + return new ParseCommandRunner(mockHttpClient.Object, mockInstallationController.Object, Client.MetadataController, Client.ServerConnectionData, new Lazy(() => Client.UserController)).RunCommandAsync(new ParseCommand("endpoint", method: "GET", data: default)).ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + Assert.IsInstanceOfType(task.Result.Item2, typeof(IDictionary)); + Assert.AreEqual(0, task.Result.Item2.Count); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(CommandTests))] + public Task TestRunCommandWithArrayResult() + { + Mock mockHttpClient = new Mock(); + Mock mockInstallationController = new Mock(); + mockHttpClient.Setup(obj => obj.ExecuteAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(new Tuple(HttpStatusCode.OK, "[]"))); + + mockInstallationController.Setup(installation => installation.GetAsync()).Returns(Task.FromResult(default)); + + return new ParseCommandRunner(mockHttpClient.Object, mockInstallationController.Object, Client.MetadataController, Client.ServerConnectionData, new Lazy(() => Client.UserController)).RunCommandAsync(new ParseCommand("endpoint", method: "GET", data: default)).ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + Assert.IsInstanceOfType(task.Result.Item2, typeof(IDictionary)); + Assert.AreEqual(1, task.Result.Item2.Count); + Assert.IsTrue(task.Result.Item2.ContainsKey("results")); + Assert.IsInstanceOfType(task.Result.Item2["results"], typeof(IList)); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(CommandTests))] + public Task TestRunCommandWithInvalidString() + { + Mock mockHttpClient = new Mock(); + Mock mockInstallationController = new Mock(); + mockHttpClient.Setup(obj => obj.ExecuteAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(new Tuple(HttpStatusCode.OK, "invalid"))); + + mockInstallationController.Setup(controller => controller.GetAsync()).Returns(Task.FromResult(default)); + + return new ParseCommandRunner(mockHttpClient.Object, mockInstallationController.Object, Client.MetadataController, Client.ServerConnectionData, new Lazy(() => Client.UserController)).RunCommandAsync(new ParseCommand("endpoint", method: "GET", data: default)).ContinueWith(task => + { + Assert.IsTrue(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + Assert.IsInstanceOfType(task.Exception.InnerException, typeof(ParseFailureException)); + Assert.AreEqual(ParseFailureException.ErrorCode.OtherCause, (task.Exception.InnerException as ParseFailureException).Code); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(CommandTests))] + public Task TestRunCommandWithErrorCode() + { + Mock mockHttpClient = new Mock(); + Mock mockInstallationController = new Mock(); + mockHttpClient.Setup(obj => obj.ExecuteAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(new Tuple(HttpStatusCode.NotFound, "{ \"code\": 101, \"error\": \"Object not found.\" }"))); + + mockInstallationController.Setup(controller => controller.GetAsync()).Returns(Task.FromResult(default)); + + return new ParseCommandRunner(mockHttpClient.Object, mockInstallationController.Object, Client.MetadataController, Client.ServerConnectionData, new Lazy(() => Client.UserController)).RunCommandAsync(new ParseCommand("endpoint", method: "GET", data: default)).ContinueWith(task => + { + Assert.IsTrue(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + Assert.IsInstanceOfType(task.Exception.InnerException, typeof(ParseFailureException)); + ParseFailureException parseException = task.Exception.InnerException as ParseFailureException; + Assert.AreEqual(ParseFailureException.ErrorCode.ObjectNotFound, parseException.Code); + Assert.AreEqual("Object not found.", parseException.Message); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(CommandTests))] + public Task TestRunCommandWithInternalServerError() + { + Mock mockHttpClient = new Mock(); + Mock mockInstallationController = new Mock(); + + mockHttpClient.Setup(client => client.ExecuteAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(new Tuple(HttpStatusCode.InternalServerError, default))); + mockInstallationController.Setup(installationController => installationController.GetAsync()).Returns(Task.FromResult(default)); + + return new ParseCommandRunner(mockHttpClient.Object, mockInstallationController.Object, Client.MetadataController, Client.ServerConnectionData, new Lazy(() => Client.UserController)).RunCommandAsync(new ParseCommand("endpoint", method: "GET", data: default)).ContinueWith(task => + { + Assert.IsTrue(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + Assert.IsInstanceOfType(task.Exception.InnerException, typeof(ParseFailureException)); + Assert.AreEqual(ParseFailureException.ErrorCode.InternalServerError, (task.Exception.InnerException as ParseFailureException).Code); + }); + } + } +} diff --git a/Parse.Tests/ConfigTests.cs b/Parse.Tests/ConfigTests.cs new file mode 100644 index 00000000..a6c45f3c --- /dev/null +++ b/Parse.Tests/ConfigTests.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Newtonsoft.Json; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Platform.Configuration; +using Parse.Abstractions.Platform.Users; +using Parse.Infrastructure; +using Parse.Platform.Configuration; + +namespace Parse.Tests +{ + [TestClass] + public class ConfigTests + { + ParseClient Client { get; } = new ParseClient(new ServerConnectionData { Test = true }, new MutableServiceHub { }); + + IParseConfigurationController MockedConfigController + { + get + { + Mock mockedConfigController = new Mock(); + Mock mockedCurrentConfigController = new Mock(); + + ParseConfiguration theConfig = Client.BuildConfiguration(new Dictionary { ["params"] = new Dictionary { ["testKey"] = "testValue" } }); + + mockedCurrentConfigController.Setup(obj => obj.GetCurrentConfigAsync(Client)).Returns(Task.FromResult(theConfig)); + + mockedConfigController.Setup(obj => obj.CurrentConfigurationController).Returns(mockedCurrentConfigController.Object); + + TaskCompletionSource tcs = new TaskCompletionSource(); + tcs.TrySetCanceled(); + + mockedConfigController.Setup(obj => obj.FetchConfigAsync(It.IsAny(), It.IsAny(), It.Is(ct => ct.IsCancellationRequested))).Returns(tcs.Task); + + mockedConfigController.Setup(obj => obj.FetchConfigAsync(It.IsAny(), It.IsAny(), It.Is(ct => !ct.IsCancellationRequested))).Returns(Task.FromResult(theConfig)); + + return mockedConfigController.Object; + } + } + + [TestInitialize] + public void SetUp() => (Client.Services as OrchestrationServiceHub).Custom = new MutableServiceHub { ConfigurationController = MockedConfigController, CurrentUserController = new Mock().Object }; + + [TestCleanup] + public void TearDown() => ((Client.Services as OrchestrationServiceHub).Default as ServiceHub).Reset(); + + [TestMethod] + public void TestCurrentConfig() + { + ParseConfiguration config = Client.GetCurrentConfiguration(); + + Assert.AreEqual("testValue", config["testKey"]); + Assert.AreEqual("testValue", config.Get("testKey")); + } + + [TestMethod] + public void TestToJSON() + { + IDictionary expectedJson = new Dictionary { { "params", new Dictionary { { "testKey", "testValue" } } } }; + Assert.AreEqual(JsonConvert.SerializeObject((Client.GetCurrentConfiguration() as IJsonConvertible).ConvertToJSON()), JsonConvert.SerializeObject(expectedJson)); + } + + [TestMethod] + [AsyncStateMachine(typeof(ConfigTests))] + public Task TestGetConfig() => Client.GetConfigurationAsync().ContinueWith(task => + { + Assert.AreEqual("testValue", task.Result["testKey"]); + Assert.AreEqual("testValue", task.Result.Get("testKey")); + }); + + [TestMethod] + [AsyncStateMachine(typeof(ConfigTests))] + public Task TestGetConfigCancel() + { + CancellationTokenSource tokenSource = new CancellationTokenSource { }; + tokenSource.Cancel(); + + return Client.GetConfigurationAsync(tokenSource.Token).ContinueWith(task => Assert.IsTrue(task.IsCanceled)); + } + } +} diff --git a/Parse.Test/ConversionTests.cs b/Parse.Tests/ConversionTests.cs similarity index 84% rename from Parse.Test/ConversionTests.cs rename to Parse.Tests/ConversionTests.cs index 053a3556..2bd27fdc 100644 --- a/Parse.Test/ConversionTests.cs +++ b/Parse.Tests/ConversionTests.cs @@ -1,10 +1,8 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Parse.Utilities; +using Parse.Infrastructure.Utilities; -namespace Parse.Test +namespace Parse.Tests { [TestClass] public class ConversionTests @@ -27,7 +25,7 @@ public void TestConvertToFloatUsingNonInvariantNumberFormat() try { float inputValue = 1234.56f; - string jsonEncoded = Common.Internal.Json.Encode(inputValue); + string jsonEncoded = JsonUtilities.Encode(inputValue); float convertedValue = (float) Conversion.ConvertTo(jsonEncoded); Assert.IsTrue(inputValue == convertedValue); } diff --git a/Parse.Test/CurrentUserControllerTests.cs b/Parse.Tests/CurrentUserControllerTests.cs similarity index 50% rename from Parse.Test/CurrentUserControllerTests.cs rename to Parse.Tests/CurrentUserControllerTests.cs index 8e37c946..5de620d8 100644 --- a/Parse.Test/CurrentUserControllerTests.cs +++ b/Parse.Tests/CurrentUserControllerTests.cs @@ -1,39 +1,46 @@ -using Moq; -using Parse; -using Parse.Common.Internal; -using Parse.Core.Internal; using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Parse.Infrastructure; +using Parse.Abstractions.Infrastructure; +using Parse.Infrastructure.Utilities; +using Parse.Platform.Objects; +using Parse.Platform.Users; -namespace Parse.Test +namespace Parse.Tests { [TestClass] public class CurrentUserControllerTests { + ParseClient Client { get; } = new ParseClient(new ServerConnectionData { Test = true }); + [TestInitialize] - public void SetUp() => ParseObject.RegisterSubclass(); + public void SetUp() => Client.AddValidClass(); [TestCleanup] - public void TearDown() => ParseCorePlugins.Instance.Reset(); + public void TearDown() => (Client.Services as ServiceHub).Reset(); [TestMethod] - public void TestConstructor() => Assert.IsNull(new ParseCurrentUserController(new Mock().Object).CurrentUser); + public void TestConstructor() => Assert.IsNull(new ParseCurrentUserController(new Mock { }.Object, Client.ClassController, Client.Decoder).CurrentUser); [TestMethod] [AsyncStateMachine(typeof(CurrentUserControllerTests))] public Task TestGetSetAsync() { - Mock storageController = new Mock(MockBehavior.Strict); - Mock> mockedStorage = new Mock>(); - ParseCurrentUserController controller = new ParseCurrentUserController(storageController.Object); - ParseUser user = new ParseUser(); +#warning This method may need a fully custom ParseClient setup. + + Mock storageController = new Mock(MockBehavior.Strict); + Mock> mockedStorage = new Mock>(); + + ParseCurrentUserController controller = new ParseCurrentUserController(storageController.Object, Client.ClassController, Client.Decoder); - storageController.Setup(s => s.LoadAsync()).Returns(Task.FromResult(mockedStorage.Object)); + ParseUser user = new ParseUser { }.Bind(Client) as ParseUser; + + storageController.Setup(storage => storage.LoadAsync()).Returns(Task.FromResult(mockedStorage.Object)); return controller.SetAsync(user, CancellationToken.None).OnSuccess(_ => { @@ -48,21 +55,19 @@ public Task TestGetSetAsync() }; #pragma warning restore IDE0039 // Use local function - mockedStorage.Verify(s => s.AddAsync("CurrentUser", Match.Create(predicate))); - mockedStorage.Setup(s => s.TryGetValue("CurrentUser", out jsonObject)).Returns(true); + mockedStorage.Verify(storage => storage.AddAsync("CurrentUser", Match.Create(predicate))); + mockedStorage.Setup(storage => storage.TryGetValue("CurrentUser", out jsonObject)).Returns(true); - return controller.GetAsync(CancellationToken.None); - }).Unwrap() - .OnSuccess(t => + return controller.GetAsync(Client, CancellationToken.None); + }).Unwrap().OnSuccess(task => { Assert.AreEqual(user, controller.CurrentUser); controller.ClearFromMemory(); Assert.AreNotEqual(user, controller.CurrentUser); - return controller.GetAsync(CancellationToken.None); - }).Unwrap() - .OnSuccess(t => + return controller.GetAsync(Client, CancellationToken.None); + }).Unwrap().OnSuccess(task => { Assert.AreNotSame(user, controller.CurrentUser); Assert.IsNotNull(controller.CurrentUser); @@ -73,43 +78,40 @@ public Task TestGetSetAsync() [AsyncStateMachine(typeof(CurrentUserControllerTests))] public Task TestExistsAsync() { - Mock storageController = new Mock(); - Mock> mockedStorage = new Mock>(); - ParseCurrentUserController controller = new ParseCurrentUserController(storageController.Object); - ParseUser user = new ParseUser(); + Mock storageController = new Mock(); + Mock> mockedStorage = new Mock>(); + ParseCurrentUserController controller = new ParseCurrentUserController(storageController.Object, Client.ClassController, Client.Decoder); + ParseUser user = new ParseUser { }.Bind(Client) as ParseUser; storageController.Setup(c => c.LoadAsync()).Returns(Task.FromResult(mockedStorage.Object)); bool contains = false; - mockedStorage.Setup(s => s.AddAsync("CurrentUser", It.IsAny())).Callback(() => contains = true).Returns(Task.FromResult(null)).Verifiable(); + mockedStorage.Setup(storage => storage.AddAsync("CurrentUser", It.IsAny())).Callback(() => contains = true).Returns(Task.FromResult(null)).Verifiable(); - mockedStorage.Setup(s => s.RemoveAsync("CurrentUser")).Callback(() => contains = false).Returns(Task.FromResult(null)).Verifiable(); + mockedStorage.Setup(storage => storage.RemoveAsync("CurrentUser")).Callback(() => contains = false).Returns(Task.FromResult(null)).Verifiable(); - mockedStorage.Setup(s => s.ContainsKey("CurrentUser")).Returns(() => contains); + mockedStorage.Setup(storage => storage.ContainsKey("CurrentUser")).Returns(() => contains); return controller.SetAsync(user, CancellationToken.None).OnSuccess(_ => { Assert.AreEqual(user, controller.CurrentUser); return controller.ExistsAsync(CancellationToken.None); - }).Unwrap() - .OnSuccess(t => + }).Unwrap().OnSuccess(task => { - Assert.IsTrue(t.Result); + Assert.IsTrue(task.Result); controller.ClearFromMemory(); return controller.ExistsAsync(CancellationToken.None); - }).Unwrap() - .OnSuccess(t => + }).Unwrap().OnSuccess(task => { - Assert.IsTrue(t.Result); + Assert.IsTrue(task.Result); controller.ClearFromDisk(); return controller.ExistsAsync(CancellationToken.None); - }).Unwrap() - .OnSuccess(t => + }).Unwrap().OnSuccess(task => { - Assert.IsFalse(t.Result); + Assert.IsFalse(task.Result); mockedStorage.Verify(); }); } @@ -118,14 +120,15 @@ public Task TestExistsAsync() [AsyncStateMachine(typeof(CurrentUserControllerTests))] public Task TestIsCurrent() { - Mock storageController = new Mock(MockBehavior.Strict); - ParseCurrentUserController controller = new ParseCurrentUserController(storageController.Object); - ParseUser user = new ParseUser(); - ParseUser user2 = new ParseUser(); + Mock storageController = new Mock(MockBehavior.Strict); + ParseCurrentUserController controller = new ParseCurrentUserController(storageController.Object, Client.ClassController, Client.Decoder); + + ParseUser user = new ParseUser { }.Bind(Client) as ParseUser; + ParseUser user2 = new ParseUser { }.Bind(Client) as ParseUser; - storageController.Setup(s => s.LoadAsync()).Returns(Task.FromResult(new Mock>().Object)); + storageController.Setup(storage => storage.LoadAsync()).Returns(Task.FromResult(new Mock>().Object)); - return controller.SetAsync(user, CancellationToken.None).OnSuccess(t => + return controller.SetAsync(user, CancellationToken.None).OnSuccess(task => { Assert.IsTrue(controller.IsCurrent(user)); Assert.IsFalse(controller.IsCurrent(user2)); @@ -135,8 +138,7 @@ public Task TestIsCurrent() Assert.IsFalse(controller.IsCurrent(user)); return controller.SetAsync(user, CancellationToken.None); - }).Unwrap() - .OnSuccess(t => + }).Unwrap().OnSuccess(task => { Assert.IsTrue(controller.IsCurrent(user)); Assert.IsFalse(controller.IsCurrent(user2)); @@ -146,8 +148,7 @@ public Task TestIsCurrent() Assert.IsFalse(controller.IsCurrent(user)); return controller.SetAsync(user2, CancellationToken.None); - }).Unwrap() - .OnSuccess(t => + }).Unwrap().OnSuccess(task => { Assert.IsFalse(controller.IsCurrent(user)); Assert.IsTrue(controller.IsCurrent(user2)); @@ -158,49 +159,43 @@ public Task TestIsCurrent() [AsyncStateMachine(typeof(CurrentUserControllerTests))] public Task TestCurrentSessionToken() { - Mock storageController = new Mock(); - Mock> mockedStorage = new Mock>(); - ParseCurrentUserController controller = new ParseCurrentUserController(storageController.Object); + Mock storageController = new Mock(); + Mock> mockedStorage = new Mock>(); + ParseCurrentUserController controller = new ParseCurrentUserController(storageController.Object, Client.ClassController, Client.Decoder); storageController.Setup(c => c.LoadAsync()).Returns(Task.FromResult(mockedStorage.Object)); - return controller.GetCurrentSessionTokenAsync(CancellationToken.None).OnSuccess(t => + return controller.GetCurrentSessionTokenAsync(Client, CancellationToken.None).OnSuccess(task => { - Assert.IsNull(t.Result); + Assert.IsNull(task.Result); // We should probably mock this. - ParseUser user = ParseObject.CreateWithoutData(null); + + ParseUser user = Client.CreateObjectWithoutData(default); user.HandleFetchResult(new MutableObjectState { ServerData = new Dictionary { ["sessionToken"] = "randomString" } }); return controller.SetAsync(user, CancellationToken.None); - }).Unwrap() - .OnSuccess(_ => controller.GetCurrentSessionTokenAsync(CancellationToken.None)).Unwrap() - .OnSuccess(t => Assert.AreEqual("randomString", t.Result)); + }).Unwrap().OnSuccess(_ => controller.GetCurrentSessionTokenAsync(Client, CancellationToken.None)).Unwrap().OnSuccess(task => Assert.AreEqual("randomString", task.Result)); } public Task TestLogOut() { - ParseCurrentUserController controller = new ParseCurrentUserController(new Mock(MockBehavior.Strict).Object); - ParseUser user = new ParseUser(); + ParseCurrentUserController controller = new ParseCurrentUserController(new Mock(MockBehavior.Strict).Object, Client.ClassController, Client.Decoder); + ParseUser user = new ParseUser { }.Bind(Client) as ParseUser; return controller.SetAsync(user, CancellationToken.None).OnSuccess(_ => { Assert.AreEqual(user, controller.CurrentUser); return controller.ExistsAsync(CancellationToken.None); - }).Unwrap() - .OnSuccess(t => + }).Unwrap().OnSuccess(task => { - Assert.IsTrue(t.Result); - - return controller.LogOutAsync(CancellationToken.None); - }).Unwrap().OnSuccess(_ => controller.GetAsync(CancellationToken.None)).Unwrap() - .OnSuccess(t => + Assert.IsTrue(task.Result); + return controller.LogOutAsync(Client, CancellationToken.None); + }).Unwrap().OnSuccess(_ => controller.GetAsync(Client, CancellationToken.None)).Unwrap().OnSuccess(task => { - Assert.IsNull(t.Result); - + Assert.IsNull(task.Result); return controller.ExistsAsync(CancellationToken.None); - }).Unwrap() - .OnSuccess(t => Assert.IsFalse(t.Result)); + }).Unwrap().OnSuccess(t => Assert.IsFalse(t.Result)); } } } diff --git a/Parse.Test/DecoderTests.cs b/Parse.Tests/DecoderTests.cs similarity index 67% rename from Parse.Test/DecoderTests.cs rename to Parse.Tests/DecoderTests.cs index ebff90d1..c074cb83 100644 --- a/Parse.Test/DecoderTests.cs +++ b/Parse.Tests/DecoderTests.cs @@ -1,19 +1,22 @@ -using Parse; -using Parse.Core.Internal; using System; using System.Collections.Generic; - using Microsoft.VisualStudio.TestTools.UnitTesting; +using Parse.Abstractions.Internal; +using Parse.Infrastructure; +using Parse.Infrastructure.Data; -namespace Parse.Test +namespace Parse.Tests { [TestClass] public class DecoderTests { + ParseClient Client { get; } = new ParseClient(new ServerConnectionData { Test = true }); + [TestMethod] public void TestParseDate() { - DateTime dateTime = (DateTime) ParseDecoder.Instance.Decode(ParseDecoder.ParseDate("1990-08-30T12:03:59.000Z")); + DateTime dateTime = (DateTime) Client.Decoder.Decode(ParseDataDecoder.ParseDate("1990-08-30T12:03:59.000Z"), Client); + Assert.AreEqual(1990, dateTime.Year); Assert.AreEqual(8, dateTime.Month); Assert.AreEqual(30, dateTime.Day); @@ -26,21 +29,22 @@ public void TestParseDate() [TestMethod] public void TestDecodePrimitives() { - Assert.AreEqual(1, ParseDecoder.Instance.Decode(1)); - Assert.AreEqual(0.3, ParseDecoder.Instance.Decode(0.3)); - Assert.AreEqual("halyosy", ParseDecoder.Instance.Decode("halyosy")); + Assert.AreEqual(1, Client.Decoder.Decode(1, Client)); + Assert.AreEqual(0.3, Client.Decoder.Decode(0.3, Client)); + Assert.AreEqual("halyosy", Client.Decoder.Decode("halyosy", Client)); - Assert.IsNull(ParseDecoder.Instance.Decode(null)); + Assert.IsNull(Client.Decoder.Decode(default, Client)); } [TestMethod] // Decoding ParseFieldOperation is not supported on .NET now. We only need this for LDS. - public void TestDecodeFieldOperation() => Assert.ThrowsException(() => ParseDecoder.Instance.Decode(new Dictionary() { { "__op", "Increment" }, { "amount", "322" } })); + public void TestDecodeFieldOperation() => Assert.ThrowsException(() => Client.Decoder.Decode(new Dictionary { { "__op", "Increment" }, { "amount", "322" } }, Client)); [TestMethod] public void TestDecodeDate() { - DateTime dateTime = (DateTime) ParseDecoder.Instance.Decode(new Dictionary() { { "__type", "Date" }, { "iso", "1990-08-30T12:03:59.000Z" } }); + DateTime dateTime = (DateTime) Client.Decoder.Decode(new Dictionary { { "__type", "Date" }, { "iso", "1990-08-30T12:03:59.000Z" } }, Client); + Assert.AreEqual(1990, dateTime.Year); Assert.AreEqual(8, dateTime.Month); Assert.AreEqual(30, dateTime.Day); @@ -57,7 +61,8 @@ public void TestDecodeImproperDate() for (int i = 0; i < 2; i++, value["iso"] = (value["iso"] as string).Substring(0, (value["iso"] as string).Length - 1) + "0Z") { - DateTime dateTime = (DateTime) ParseDecoder.Instance.Decode(value); + DateTime dateTime = (DateTime) Client.Decoder.Decode(value, Client); + Assert.AreEqual(1990, dateTime.Year); Assert.AreEqual(8, dateTime.Month); Assert.AreEqual(30, dateTime.Day); @@ -69,12 +74,13 @@ public void TestDecodeImproperDate() } [TestMethod] - public void TestDecodeBytes() => Assert.AreEqual("This is an encoded string", System.Text.Encoding.UTF8.GetString(ParseDecoder.Instance.Decode(new Dictionary() { { "__type", "Bytes" }, { "base64", "VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw==" } }) as byte[])); + public void TestDecodeBytes() => Assert.AreEqual("This is an encoded string", System.Text.Encoding.UTF8.GetString(Client.Decoder.Decode(new Dictionary { { "__type", "Bytes" }, { "base64", "VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw==" } }, Client) as byte[])); [TestMethod] public void TestDecodePointer() { - ParseObject obj = ParseDecoder.Instance.Decode(new Dictionary { ["__type"] = "Pointer", ["className"] = "Corgi", ["objectId"] = "lLaKcolnu" }) as ParseObject; + ParseObject obj = Client.Decoder.Decode(new Dictionary { ["__type"] = "Pointer", ["className"] = "Corgi", ["objectId"] = "lLaKcolnu" }, Client) as ParseObject; + Assert.IsFalse(obj.IsDataAvailable); Assert.AreEqual("Corgi", obj.ClassName); Assert.AreEqual("lLaKcolnu", obj.ObjectId); @@ -84,23 +90,25 @@ public void TestDecodePointer() public void TestDecodeFile() { - ParseFile file1 = ParseDecoder.Instance.Decode(new Dictionary { ["__type"] = "File", ["name"] = "Corgi.png", ["url"] = "http://corgi.xyz/gogo.png" }) as ParseFile; + ParseFile file1 = Client.Decoder.Decode(new Dictionary { ["__type"] = "File", ["name"] = "Corgi.png", ["url"] = "http://corgi.xyz/gogo.png" }, Client) as ParseFile; + Assert.AreEqual("Corgi.png", file1.Name); Assert.AreEqual("http://corgi.xyz/gogo.png", file1.Url.AbsoluteUri); Assert.IsFalse(file1.IsDirty); - Assert.ThrowsException(() => ParseDecoder.Instance.Decode(new Dictionary { ["__type"] = "File", ["name"] = "Corgi.png" })); + Assert.ThrowsException(() => Client.Decoder.Decode(new Dictionary { ["__type"] = "File", ["name"] = "Corgi.png" }, Client)); } [TestMethod] public void TestDecodeGeoPoint() { - ParseGeoPoint point1 = (ParseGeoPoint) ParseDecoder.Instance.Decode(new Dictionary { ["__type"] = "GeoPoint", ["latitude"] = 0.9, ["longitude"] = 0.3 }); + ParseGeoPoint point1 = (ParseGeoPoint) Client.Decoder.Decode(new Dictionary { ["__type"] = "GeoPoint", ["latitude"] = 0.9, ["longitude"] = 0.3 }, Client); + Assert.IsNotNull(point1); Assert.AreEqual(0.9, point1.Latitude); Assert.AreEqual(0.3, point1.Longitude); - Assert.ThrowsException(() => ParseDecoder.Instance.Decode(new Dictionary { ["__type"] = "GeoPoint", ["latitude"] = 0.9 })); + Assert.ThrowsException(() => Client.Decoder.Decode(new Dictionary { ["__type"] = "GeoPoint", ["latitude"] = 0.9 }, Client)); } [TestMethod] @@ -115,7 +123,8 @@ public void TestDecodeObject() ["updatedAt"] = "2015-06-22T22:06:41.733Z" }; - ParseObject obj = ParseDecoder.Instance.Decode(value) as ParseObject; + ParseObject obj = Client.Decoder.Decode(value, Client) as ParseObject; + Assert.IsTrue(obj.IsDataAvailable); Assert.AreEqual("Corgi", obj.ClassName); Assert.AreEqual("lLaKcolnu", obj.ObjectId); @@ -133,7 +142,8 @@ public void TestDecodeRelation() ["objectId"] = "lLaKcolnu" }; - ParseRelation relation = ParseDecoder.Instance.Decode(value) as ParseRelation; + ParseRelation relation = Client.Decoder.Decode(value, Client) as ParseRelation; + Assert.IsNotNull(relation); Assert.AreEqual("Corgi", relation.GetTargetClassName()); } @@ -162,7 +172,8 @@ public void TestDecodeDictionary() } }; - IDictionary dict = ParseDecoder.Instance.Decode(value) as IDictionary; + IDictionary dict = Client.Decoder.Decode(value, Client) as IDictionary; + Assert.AreEqual("luka", dict["megurine"]); Assert.IsTrue(dict["hatsune"] is ParseObject); Assert.IsTrue(dict["decodedGeoPoint"] is ParseGeoPoint); @@ -173,10 +184,11 @@ public void TestDecodeDictionary() IDictionary randomValue = new Dictionary() { ["ultimate"] = "elements", - [new ParseACL()] = "lLaKcolnu" + [new ParseACL { }] = "lLaKcolnu" }; - IDictionary randomDict = ParseDecoder.Instance.Decode(randomValue) as IDictionary; + IDictionary randomDict = Client.Decoder.Decode(randomValue, Client) as IDictionary; + Assert.AreEqual("elements", randomDict["ultimate"]); Assert.AreEqual(2, randomDict.Keys.Count); } @@ -184,18 +196,18 @@ [new ParseACL()] = "lLaKcolnu" [TestMethod] public void TestDecodeList() { - IList value = new List() + IList value = new List { - 1, new ParseACL(), "wiz", - new Dictionary() + 1, new ParseACL { }, "wiz", + new Dictionary { ["__type"] = "GeoPoint", ["latitude"] = 0.9, ["longitude"] = 0.3 }, - new List() + new List { - new Dictionary() + new Dictionary { ["__type"] = "GeoPoint", ["latitude"] = 0.9, @@ -204,7 +216,8 @@ public void TestDecodeList() } }; - IList list = ParseDecoder.Instance.Decode(value) as IList; + IList list = Client.Decoder.Decode(value, Client) as IList; + Assert.AreEqual(1, list[0]); Assert.IsTrue(list[1] is ParseACL); Assert.AreEqual("wiz", list[2]); @@ -217,9 +230,8 @@ public void TestDecodeList() [TestMethod] public void TestDecodeArray() { - int[] value = new int[] { 1, 2, 3, 4 }; + int[] value = new int[] { 1, 2, 3, 4 }, array = Client.Decoder.Decode(value, Client) as int[]; - int[] array = ParseDecoder.Instance.Decode(value) as int[]; Assert.AreEqual(4, array.Length); Assert.AreEqual(1, array[0]); Assert.AreEqual(2, array[1]); diff --git a/Parse.Test/EncoderTests.cs b/Parse.Tests/EncoderTests.cs similarity index 72% rename from Parse.Test/EncoderTests.cs rename to Parse.Tests/EncoderTests.cs index e6b3624c..505938de 100644 --- a/Parse.Test/EncoderTests.cs +++ b/Parse.Tests/EncoderTests.cs @@ -1,61 +1,65 @@ -using Parse; -using Parse.Core.Internal; using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; +using Parse.Abstractions.Internal; +using Parse.Infrastructure; +using Parse.Infrastructure.Control; +using Parse.Infrastructure.Data; // TODO (hallucinogen): mock ParseACL, ParseObject, ParseUser once we have their Interfaces -namespace Parse.Test +namespace Parse.Tests { [TestClass] public class EncoderTests { + ParseClient Client { get; } = new ParseClient(new ServerConnectionData { Test = true }); + /// - /// A that's used only for testing. This class is used to test - /// 's base methods. + /// A that's used only for testing. This class is used to test + /// 's base methods. /// - private class ParseEncoderTestClass : ParseEncoder + class ParseEncoderTestClass : ParseDataEncoder { - private static readonly ParseEncoderTestClass instance = new ParseEncoderTestClass(); - public static ParseEncoderTestClass Instance => instance; + public static ParseEncoderTestClass Instance { get; } = new ParseEncoderTestClass { }; - protected override IDictionary EncodeParseObject(ParseObject value) => null; + protected override IDictionary EncodeObject(ParseObject value) => null; } [TestMethod] public void TestIsValidType() { ParseObject corgi = new ParseObject("Corgi"); - ParseRelation corgiRelation = corgi.GetRelation("corgi"); - - Assert.IsTrue(ParseEncoder.IsValidType(322)); - Assert.IsTrue(ParseEncoder.IsValidType(0.3f)); - Assert.IsTrue(ParseEncoder.IsValidType(new byte[] { 1, 2, 3, 4 })); - Assert.IsTrue(ParseEncoder.IsValidType("corgi")); - Assert.IsTrue(ParseEncoder.IsValidType(corgi)); - Assert.IsTrue(ParseEncoder.IsValidType(new ParseACL())); - Assert.IsTrue(ParseEncoder.IsValidType(new ParseFile("Corgi", new byte[0]))); - Assert.IsTrue(ParseEncoder.IsValidType(new ParseGeoPoint(1, 2))); - Assert.IsTrue(ParseEncoder.IsValidType(corgiRelation)); - Assert.IsTrue(ParseEncoder.IsValidType(new DateTime())); - Assert.IsTrue(ParseEncoder.IsValidType(new List())); - Assert.IsTrue(ParseEncoder.IsValidType(new Dictionary())); - Assert.IsTrue(ParseEncoder.IsValidType(new Dictionary())); - - Assert.IsFalse(ParseEncoder.IsValidType(new ParseAddOperation(new List()))); - Assert.IsFalse(ParseEncoder.IsValidType(Task.FromResult(new ParseObject("Corgi")))); - Assert.ThrowsException(() => ParseEncoder.IsValidType(new Dictionary())); - Assert.ThrowsException(() => ParseEncoder.IsValidType(new Dictionary())); + ParseRelation corgiRelation = corgi.GetRelation(nameof(corgi)); + + Assert.IsTrue(ParseDataEncoder.Validate(322)); + Assert.IsTrue(ParseDataEncoder.Validate(0.3f)); + Assert.IsTrue(ParseDataEncoder.Validate(new byte[] { 1, 2, 3, 4 })); + Assert.IsTrue(ParseDataEncoder.Validate(nameof(corgi))); + Assert.IsTrue(ParseDataEncoder.Validate(corgi)); + Assert.IsTrue(ParseDataEncoder.Validate(new ParseACL { })); + Assert.IsTrue(ParseDataEncoder.Validate(new ParseFile("Corgi", new byte[0]))); + Assert.IsTrue(ParseDataEncoder.Validate(new ParseGeoPoint(1, 2))); + Assert.IsTrue(ParseDataEncoder.Validate(corgiRelation)); + Assert.IsTrue(ParseDataEncoder.Validate(new DateTime { })); + Assert.IsTrue(ParseDataEncoder.Validate(new List { })); + Assert.IsTrue(ParseDataEncoder.Validate(new Dictionary { })); + Assert.IsTrue(ParseDataEncoder.Validate(new Dictionary { })); + + Assert.IsFalse(ParseDataEncoder.Validate(new ParseAddOperation(new List { }))); + Assert.IsFalse(ParseDataEncoder.Validate(Task.FromResult(new ParseObject("Corgi")))); + Assert.ThrowsException(() => ParseDataEncoder.Validate(new Dictionary { })); + Assert.ThrowsException(() => ParseDataEncoder.Validate(new Dictionary { })); } [TestMethod] public void TestEncodeDate() { DateTime dateTime = new DateTime(1990, 8, 30, 12, 3, 59); - IDictionary value = ParseEncoderTestClass.Instance.Encode(dateTime) as IDictionary; + + IDictionary value = ParseEncoderTestClass.Instance.Encode(dateTime, Client) as IDictionary; + Assert.AreEqual("Date", value["__type"]); Assert.AreEqual("1990-08-30T12:03:59.000Z", value["iso"]); } @@ -64,7 +68,9 @@ public void TestEncodeDate() public void TestEncodeBytes() { byte[] bytes = new byte[] { 1, 2, 3, 4 }; - IDictionary value = ParseEncoderTestClass.Instance.Encode(bytes) as IDictionary; + + IDictionary value = ParseEncoderTestClass.Instance.Encode(bytes, Client) as IDictionary; + Assert.AreEqual("Bytes", value["__type"]); Assert.AreEqual(Convert.ToBase64String(new byte[] { 1, 2, 3, 4 }), value["base64"]); } @@ -73,7 +79,8 @@ public void TestEncodeBytes() public void TestEncodeParseObjectWithNoObjectsEncoder() { ParseObject obj = new ParseObject("Corgi"); - Assert.ThrowsException(() => NoObjectsEncoder.Instance.Encode(obj)); + + Assert.ThrowsException(() => NoObjectsEncoder.Instance.Encode(obj, Client)); } [TestMethod] @@ -86,20 +93,25 @@ public void TestEncodeParseObjectWithPointerOrLocalIdEncoder() public void TestEncodeParseFile() { ParseFile file1 = ParseFileExtensions.Create("Corgi.png", new Uri("http://corgi.xyz/gogo.png")); - IDictionary value = ParseEncoderTestClass.Instance.Encode(file1) as IDictionary; + + IDictionary value = ParseEncoderTestClass.Instance.Encode(file1, Client) as IDictionary; + Assert.AreEqual("File", value["__type"]); Assert.AreEqual("Corgi.png", value["name"]); Assert.AreEqual("http://corgi.xyz/gogo.png", value["url"]); ParseFile file2 = new ParseFile(null, new MemoryStream(new byte[] { 1, 2, 3, 4 })); - Assert.ThrowsException(() => ParseEncoderTestClass.Instance.Encode(file2)); + + Assert.ThrowsException(() => ParseEncoderTestClass.Instance.Encode(file2, Client)); } [TestMethod] public void TestEncodeParseGeoPoint() { ParseGeoPoint point = new ParseGeoPoint(3.22, 32.2); - IDictionary value = ParseEncoderTestClass.Instance.Encode(point) as IDictionary; + + IDictionary value = ParseEncoderTestClass.Instance.Encode(point, Client) as IDictionary; + Assert.AreEqual("GeoPoint", value["__type"]); Assert.AreEqual(3.22, value["latitude"]); Assert.AreEqual(32.2, value["longitude"]); @@ -109,7 +121,9 @@ public void TestEncodeParseGeoPoint() public void TestEncodeACL() { ParseACL acl1 = new ParseACL(); - IDictionary value1 = ParseEncoderTestClass.Instance.Encode(acl1) as IDictionary; + + IDictionary value1 = ParseEncoderTestClass.Instance.Encode(acl1, Client) as IDictionary; + Assert.IsNotNull(value1); Assert.AreEqual(0, value1.Keys.Count); @@ -118,7 +132,9 @@ public void TestEncodeACL() PublicReadAccess = true, PublicWriteAccess = true }; - IDictionary value2 = ParseEncoderTestClass.Instance.Encode(acl2) as IDictionary; + + IDictionary value2 = ParseEncoderTestClass.Instance.Encode(acl2, Client) as IDictionary; + Assert.AreEqual(1, value2.Keys.Count); IDictionary publicAccess = value2["*"] as IDictionary; Assert.AreEqual(2, publicAccess.Keys.Count); @@ -133,7 +149,9 @@ public void TestEncodeParseRelation() { ParseObject obj = new ParseObject("Corgi"); ParseRelation relation = ParseRelationExtensions.Create(obj, "nano", "Husky"); - IDictionary value = ParseEncoderTestClass.Instance.Encode(relation) as IDictionary; + + IDictionary value = ParseEncoderTestClass.Instance.Encode(relation, Client) as IDictionary; + Assert.AreEqual("Relation", value["__type"]); Assert.AreEqual("Husky", value["className"]); } @@ -142,10 +160,13 @@ public void TestEncodeParseRelation() public void TestEncodeParseFieldOperation() { ParseIncrementOperation incOps = new ParseIncrementOperation(1); - IDictionary value = ParseEncoderTestClass.Instance.Encode(incOps) as IDictionary; + + IDictionary value = ParseEncoderTestClass.Instance.Encode(incOps, Client) as IDictionary; + Assert.AreEqual("Increment", value["__op"]); Assert.AreEqual(1, value["amount"]); - // Other operations are tested in FieldOperationTests + + // Other operations are tested in FieldOperationTests. } [TestMethod] @@ -165,7 +186,8 @@ public void TestEncodeList() } }; - IList value = ParseEncoderTestClass.Instance.Encode(list) as IList; + IList value = ParseEncoderTestClass.Instance.Encode(list, Client) as IList; + IDictionary item0 = value[0] as IDictionary; Assert.AreEqual("GeoPoint", item0["__type"]); Assert.AreEqual(0.0, item0["latitude"]); @@ -193,22 +215,23 @@ public void TestEncodeDictionary() IDictionary dict = new Dictionary() { ["item"] = "random", - ["list"] = new List() { "vesperia", "abyss", "legendia" }, + ["list"] = new List { "vesperia", "abyss", "legendia" }, ["array"] = new int[] { 1, 2, 3 }, ["geo"] = new ParseGeoPoint(0, 0), ["validDict"] = new Dictionary { ["phantasia"] = "jbf" } }; - IDictionary value = ParseEncoderTestClass.Instance.Encode(dict) as IDictionary; + IDictionary value = ParseEncoderTestClass.Instance.Encode(dict, Client) as IDictionary; + Assert.AreEqual("random", value["item"]); Assert.IsTrue(value["list"] is IList); Assert.IsTrue(value["array"] is IList); Assert.IsTrue(value["geo"] is IDictionary); Assert.IsTrue(value["validDict"] is IDictionary); - Assert.ThrowsException(() => ParseEncoderTestClass.Instance.Encode(new Dictionary { })); + Assert.ThrowsException(() => ParseEncoderTestClass.Instance.Encode(new Dictionary { }, Client)); - Assert.ThrowsException(() => ParseEncoderTestClass.Instance.Encode(new Dictionary { ["validDict"] = new Dictionary { [new ParseACL()] = "jbf" } })); + Assert.ThrowsException(() => ParseEncoderTestClass.Instance.Encode(new Dictionary { ["validDict"] = new Dictionary { [new ParseACL()] = "jbf" } }, Client)); } } } diff --git a/Parse.Test/FileControllerTests.cs b/Parse.Tests/FileControllerTests.cs similarity index 82% rename from Parse.Test/FileControllerTests.cs rename to Parse.Tests/FileControllerTests.cs index 2d1ac926..8d8ddbd3 100644 --- a/Parse.Test/FileControllerTests.cs +++ b/Parse.Tests/FileControllerTests.cs @@ -1,6 +1,3 @@ -using Moq; -using Parse; -using Parse.Core.Internal; using System; using System.Collections.Generic; using System.IO; @@ -8,16 +5,22 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Execution; +using Parse.Infrastructure.Execution; +using Parse.Platform.Files; -namespace Parse.Test +namespace Parse.Tests { +#warning Refactor this class. +#warning Skipped initialization step may be needed. + [TestClass] public class FileControllerTests { - [TestInitialize] - public void SetUp() => ParseClient.Initialize(new ParseClient.Configuration { ApplicationID = "", Key = "" }); + // public void SetUp() => Client = new ParseClient(new ServerConnectionData { ApplicationID = "", Key = "", Test = true }); [TestMethod] [AsyncStateMachine(typeof(FileControllerTests))] @@ -28,7 +31,7 @@ public Task TestFileControllerSaveWithInvalidResult() FileState state = new FileState { Name = "bekti.png", - MimeType = "image/png" + MediaType = "image/png" }; ParseFileController controller = new ParseFileController(mockRunner.Object); @@ -44,7 +47,7 @@ public Task TestFileControllerSaveWithEmptyResult() FileState state = new FileState { Name = "bekti.png", - MimeType = "image/png" + MediaType = "image/png" }; ParseFileController controller = new ParseFileController(mockRunner.Object); @@ -60,7 +63,7 @@ public Task TestFileControllerSaveWithIncompleteResult() FileState state = new FileState { Name = "bekti.png", - MimeType = "image/png" + MediaType = "image/png" }; ParseFileController controller = new ParseFileController(mockRunner.Object); @@ -74,7 +77,7 @@ public Task TestFileControllerSave() FileState state = new FileState { Name = "bekti.png", - MimeType = "image/png" + MediaType = "image/png" }; return new ParseFileController(CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, new Dictionary { ["name"] = "newBekti.png", ["url"] = "https://www.parse.com/newBekti.png" })).Object).SaveAsync(state, dataStream: new MemoryStream(), sessionToken: null, progress: null).ContinueWith(t => @@ -82,16 +85,16 @@ public Task TestFileControllerSave() Assert.IsFalse(t.IsFaulted); FileState newState = t.Result; - Assert.AreEqual(state.MimeType, newState.MimeType); + Assert.AreEqual(state.MediaType, newState.MediaType); Assert.AreEqual("newBekti.png", newState.Name); - Assert.AreEqual("https://www.parse.com/newBekti.png", newState.Url.AbsoluteUri); + Assert.AreEqual("https://www.parse.com/newBekti.png", newState.Location.AbsoluteUri); }); } private Mock CreateMockRunner(Tuple> response) { Mock mockRunner = new Mock(); - mockRunner.Setup(obj => obj.RunCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(response)); + mockRunner.Setup(obj => obj.RunCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(response)); return mockRunner; } diff --git a/Parse.Test/FileStateTests.cs b/Parse.Tests/FileStateTests.cs similarity index 58% rename from Parse.Test/FileStateTests.cs rename to Parse.Tests/FileStateTests.cs index 7b378d11..e624a7da 100644 --- a/Parse.Test/FileStateTests.cs +++ b/Parse.Tests/FileStateTests.cs @@ -1,9 +1,8 @@ -using Parse.Core.Internal; using System; - using Microsoft.VisualStudio.TestTools.UnitTesting; +using Parse.Platform.Files; -namespace Parse.Test +namespace Parse.Tests { [TestClass] public class FileStateTests @@ -18,25 +17,25 @@ public void TestSecureUrl() FileState state = new FileState { Name = "A", - Url = unsecureUri, - MimeType = null + Location = unsecureUri, + MediaType = null }; - Assert.AreEqual(unsecureUri, state.Url); - Assert.AreEqual(secureUri, state.SecureUrl); + Assert.AreEqual(unsecureUri, state.Location); + Assert.AreEqual(secureUri, state.SecureLocation); // Make sure the proper port was given back. - Assert.AreEqual(443, state.SecureUrl.Port); + Assert.AreEqual(443, state.SecureLocation.Port); state = new FileState { Name = "B", - Url = randomUri, - MimeType = null + Location = randomUri, + MediaType = null }; - Assert.AreEqual(randomUri, state.Url); - Assert.AreEqual(randomUri, state.Url); + Assert.AreEqual(randomUri, state.Location); + Assert.AreEqual(randomUri, state.Location); } } } diff --git a/Parse.Test/FileTests.cs b/Parse.Tests/FileTests.cs similarity index 70% rename from Parse.Test/FileTests.cs rename to Parse.Tests/FileTests.cs index 6e287cde..86b1745c 100644 --- a/Parse.Test/FileTests.cs +++ b/Parse.Tests/FileTests.cs @@ -1,39 +1,41 @@ -using Moq; -using Parse; -using Parse.Core.Internal; using System; using System.IO; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Parse.Test +using Moq; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Internal; +using Parse.Abstractions.Platform.Files; +using Parse.Abstractions.Platform.Users; +using Parse.Infrastructure; +using Parse.Platform.Files; + +namespace Parse.Tests { [TestClass] public class FileTests { - [TestCleanup] - public void TearDown() => ParseCorePlugins.Instance = null; - [TestMethod] [AsyncStateMachine(typeof(FileTests))] public Task TestFileSave() { Mock mockController = new Mock(); - mockController.Setup(obj => obj.SaveAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(new FileState { Name = "newBekti.png", Url = new Uri("https://www.parse.com/newBekti.png"), MimeType = "image/png" })); + mockController.Setup(obj => obj.SaveAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(new FileState { Name = "newBekti.png", Location = new Uri("https://www.parse.com/newBekti.png"), MediaType = "image/png" })); Mock mockCurrentUserController = new Mock(); - ParseCorePlugins.Instance = new ParseCorePlugins { FileController = mockController.Object, CurrentUserController = mockCurrentUserController.Object }; - ParseFile file = new ParseFile("bekti.jpeg", new MemoryStream(), "image/jpeg"); + ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, new MutableServiceHub { FileController = mockController.Object, CurrentUserController = mockCurrentUserController.Object }); + + ParseFile file = new ParseFile("bekti.jpeg", new MemoryStream { }, "image/jpeg"); + Assert.AreEqual("bekti.jpeg", file.Name); Assert.AreEqual("image/jpeg", file.MimeType); Assert.IsTrue(file.IsDirty); - return file.SaveAsync().ContinueWith(t => + return file.SaveAsync(client).ContinueWith(task => { - Assert.IsFalse(t.IsFaulted); + Assert.IsFalse(task.IsFaulted); Assert.AreEqual("newBekti.png", file.Name); Assert.AreEqual("image/png", file.MimeType); Assert.AreEqual("https://www.parse.com/newBekti.png", file.Url.AbsoluteUri); diff --git a/Parse.Test/GeoPointTests.cs b/Parse.Tests/GeoPointTests.cs similarity index 83% rename from Parse.Test/GeoPointTests.cs rename to Parse.Tests/GeoPointTests.cs index fef1b8df..a9611042 100644 --- a/Parse.Test/GeoPointTests.cs +++ b/Parse.Tests/GeoPointTests.cs @@ -1,32 +1,37 @@ -using Parse; -using Parse.Common.Internal; -using Parse.Core.Internal; using System; using System.Collections.Generic; using System.Globalization; using System.Threading; - using Microsoft.VisualStudio.TestTools.UnitTesting; +using Parse.Infrastructure; +using Parse.Infrastructure.Data; +using Parse.Infrastructure.Utilities; -namespace Parse.Test +namespace Parse.Tests { [TestClass] public class GeoPointTests { + ParseClient Client { get; } = new ParseClient(new ServerConnectionData { Test = true }); + [TestMethod] public void TestGeoPointCultureInvariantParsing() { - CultureInfo originalCulture = Thread.CurrentThread.CurrentCulture; - foreach (CultureInfo c in CultureInfo.GetCultures(CultureTypes.AllCultures)) + CultureInfo initialCulture = Thread.CurrentThread.CurrentCulture; + + foreach (CultureInfo culture in CultureInfo.GetCultures(CultureTypes.AllCultures)) { - Thread.CurrentThread.CurrentCulture = c; + Thread.CurrentThread.CurrentCulture = culture; + ParseGeoPoint point = new ParseGeoPoint(1.234, 1.234); - string serialized = Json.Encode(new Dictionary { { "point", NoObjectsEncoder.Instance.Encode(point) } }); - IDictionary deserialized = ParseDecoder.Instance.Decode(Json.Parse(serialized)) as IDictionary; - ParseGeoPoint pointAgain = (ParseGeoPoint) deserialized["point"]; + IDictionary deserialized = Client.Decoder.Decode(JsonUtilities.Parse(JsonUtilities.Encode(new Dictionary { [nameof(point)] = NoObjectsEncoder.Instance.Encode(point, Client) })), Client) as IDictionary; + ParseGeoPoint pointAgain = (ParseGeoPoint) deserialized[nameof(point)]; + Assert.AreEqual(1.234, pointAgain.Latitude); Assert.AreEqual(1.234, pointAgain.Longitude); } + + Thread.CurrentThread.CurrentCulture = initialCulture; } [TestMethod] @@ -37,11 +42,13 @@ public void TestGeoPointConstructor() Assert.AreEqual(0.0, point.Longitude); point = new ParseGeoPoint(42, 36); + Assert.AreEqual(42.0, point.Latitude); Assert.AreEqual(36.0, point.Longitude); point.Latitude = 12; point.Longitude = 24; + Assert.AreEqual(12.0, point.Latitude); Assert.AreEqual(24.0, point.Longitude); } diff --git a/Parse.Test/InstallationIdControllerTests.cs b/Parse.Tests/InstallationIdControllerTests.cs similarity index 79% rename from Parse.Test/InstallationIdControllerTests.cs rename to Parse.Tests/InstallationIdControllerTests.cs index e467486e..99dc0d66 100644 --- a/Parse.Test/InstallationIdControllerTests.cs +++ b/Parse.Tests/InstallationIdControllerTests.cs @@ -1,27 +1,32 @@ -using Moq; -using Parse.Common.Internal; -using Parse.Core.Internal; using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Parse.Infrastructure; +using Parse.Abstractions.Infrastructure; +using Parse.Platform.Installations; -namespace Parse.Test +namespace Parse.Tests { +#warning Class refactoring may be required. + [TestClass] public class InstallationIdControllerTests { + ParseClient Client { get; } = new ParseClient(new ServerConnectionData { Test = true }); + [TestCleanup] - public void TearDown() => ParseCorePlugins.Instance = null; + public void TearDown() => (Client.Services as ServiceHub).Reset(); [TestMethod] public void TestConstructor() { - Mock storageMock = new Mock(MockBehavior.Strict); - InstallationIdController controller = new InstallationIdController(storageMock.Object); + Mock storageMock = new Mock(MockBehavior.Strict); + ParseInstallationController controller = new ParseInstallationController(storageMock.Object); // Make sure it didn't touch storageMock. + storageMock.Verify(); } @@ -29,12 +34,12 @@ public void TestConstructor() [AsyncStateMachine(typeof(InstallationIdControllerTests))] public Task TestGet() { - Mock storageMock = new Mock(MockBehavior.Strict); - Mock> storageDictionary = new Mock>(); + Mock storageMock = new Mock(MockBehavior.Strict); + Mock> storageDictionary = new Mock>(); storageMock.Setup(s => s.LoadAsync()).Returns(Task.FromResult(storageDictionary.Object)); - InstallationIdController controller = new InstallationIdController(storageMock.Object); + ParseInstallationController controller = new ParseInstallationController(storageMock.Object); return controller.GetAsync().ContinueWith(installationIdTask => { Assert.IsFalse(installationIdTask.IsFaulted); @@ -76,12 +81,12 @@ public Task TestGet() [AsyncStateMachine(typeof(InstallationIdControllerTests))] public Task TestSet() { - Mock storageMock = new Mock(MockBehavior.Strict); - Mock> storageDictionary = new Mock>(); + Mock storageMock = new Mock(MockBehavior.Strict); + Mock> storageDictionary = new Mock>(); storageMock.Setup(s => s.LoadAsync()).Returns(Task.FromResult(storageDictionary.Object)); - InstallationIdController controller = new InstallationIdController(storageMock.Object); + ParseInstallationController controller = new ParseInstallationController(storageMock.Object); return controller.GetAsync().ContinueWith(installationIdTask => { diff --git a/Parse.Test/InstallationTests.cs b/Parse.Tests/InstallationTests.cs similarity index 55% rename from Parse.Test/InstallationTests.cs rename to Parse.Tests/InstallationTests.cs index 9ef3c89a..88516ec5 100644 --- a/Parse.Test/InstallationTests.cs +++ b/Parse.Tests/InstallationTests.cs @@ -1,38 +1,42 @@ -using Moq; -using Parse; -using Parse.Core.Internal; -using Parse.Push.Internal; using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Platform.Installations; +using Parse.Infrastructure; +using Parse.Platform.Objects; -namespace Parse.Test +namespace Parse.Tests { [TestClass] public class InstallationTests { + ParseClient Client { get; } = new ParseClient(new ServerConnectionData { Test = true }); + [TestInitialize] - public void SetUp() => ParseObject.RegisterSubclass(); + public void SetUp() => Client.AddValidClass(); [TestCleanup] - public void TearDown() => ParseCorePlugins.Instance = null; + public void TearDown() => (Client.Services as ServiceHub).Reset(); [TestMethod] - public void TestGetInstallationQuery() => Assert.IsInstanceOfType(ParseInstallation.Query, typeof(ParseQuery)); + public void TestGetInstallationQuery() => Assert.IsInstanceOfType(Client.GetInstallationQuery(), typeof(ParseQuery)); [TestMethod] public void TestInstallationIdGetterSetter() { Guid guid = Guid.NewGuid(); - ParseInstallation installation = ParseObjectExtensions.FromState(new MutableObjectState { ServerData = new Dictionary { ["installationId"] = guid.ToString() } }, "_Installation"); + ParseInstallation installation = Client.GenerateObjectFromState(new MutableObjectState { ServerData = new Dictionary { ["installationId"] = guid.ToString() } }, "_Installation"); + Assert.IsNotNull(installation); Assert.AreEqual(guid, installation.InstallationId); Guid newGuid = Guid.NewGuid(); Assert.ThrowsException(() => installation["installationId"] = newGuid); + installation.SetIfDifferent("installationId", newGuid.ToString()); Assert.AreEqual(newGuid, installation.InstallationId); } @@ -40,11 +44,13 @@ public void TestInstallationIdGetterSetter() [TestMethod] public void TestDeviceTypeGetterSetter() { - ParseInstallation installation = ParseObjectExtensions.FromState(new MutableObjectState { ServerData = new Dictionary { ["deviceType"] = "parseOS" } }, "_Installation"); + ParseInstallation installation = Client.GenerateObjectFromState(new MutableObjectState { ServerData = new Dictionary { ["deviceType"] = "parseOS" } }, "_Installation"); + Assert.IsNotNull(installation); Assert.AreEqual("parseOS", installation.DeviceType); Assert.ThrowsException(() => installation["deviceType"] = "gogoOS"); + installation.SetIfDifferent("deviceType", "gogoOS"); Assert.AreEqual("gogoOS", installation.DeviceType); } @@ -52,11 +58,13 @@ public void TestDeviceTypeGetterSetter() [TestMethod] public void TestAppNameGetterSetter() { - ParseInstallation installation = ParseObjectExtensions.FromState(new MutableObjectState { ServerData = new Dictionary { ["appName"] = "parseApp" } }, "_Installation"); + ParseInstallation installation = Client.GenerateObjectFromState(new MutableObjectState { ServerData = new Dictionary { ["appName"] = "parseApp" } }, "_Installation"); + Assert.IsNotNull(installation); Assert.AreEqual("parseApp", installation.AppName); Assert.ThrowsException(() => installation["appName"] = "gogoApp"); + installation.SetIfDifferent("appName", "gogoApp"); Assert.AreEqual("gogoApp", installation.AppName); } @@ -64,11 +72,13 @@ public void TestAppNameGetterSetter() [TestMethod] public void TestAppVersionGetterSetter() { - ParseInstallation installation = ParseObjectExtensions.FromState(new MutableObjectState { ServerData = new Dictionary { ["appVersion"] = "1.2.3" } }, "_Installation"); + ParseInstallation installation = Client.GenerateObjectFromState(new MutableObjectState { ServerData = new Dictionary { ["appVersion"] = "1.2.3" } }, "_Installation"); + Assert.IsNotNull(installation); Assert.AreEqual("1.2.3", installation.AppVersion); Assert.ThrowsException(() => installation["appVersion"] = "1.2.4"); + installation.SetIfDifferent("appVersion", "1.2.4"); Assert.AreEqual("1.2.4", installation.AppVersion); } @@ -76,11 +86,13 @@ public void TestAppVersionGetterSetter() [TestMethod] public void TestAppIdentifierGetterSetter() { - ParseInstallation installation = ParseObjectExtensions.FromState(new MutableObjectState { ServerData = new Dictionary { ["appIdentifier"] = "com.parse.app" } }, "_Installation"); + ParseInstallation installation = Client.GenerateObjectFromState(new MutableObjectState { ServerData = new Dictionary { ["appIdentifier"] = "com.parse.app" } }, "_Installation"); + Assert.IsNotNull(installation); Assert.AreEqual("com.parse.app", installation.AppIdentifier); Assert.ThrowsException(() => installation["appIdentifier"] = "com.parse.newapp"); + installation.SetIfDifferent("appIdentifier", "com.parse.newapp"); Assert.AreEqual("com.parse.newapp", installation.AppIdentifier); } @@ -88,7 +100,8 @@ public void TestAppIdentifierGetterSetter() [TestMethod] public void TestTimeZoneGetter() { - ParseInstallation installation = ParseObjectExtensions.FromState(new MutableObjectState { ServerData = new Dictionary { ["timeZone"] = "America/Los_Angeles" } }, "_Installation"); + ParseInstallation installation = Client.GenerateObjectFromState(new MutableObjectState { ServerData = new Dictionary { ["timeZone"] = "America/Los_Angeles" } }, "_Installation"); + Assert.IsNotNull(installation); Assert.AreEqual("America/Los_Angeles", installation.TimeZone); } @@ -96,7 +109,8 @@ public void TestTimeZoneGetter() [TestMethod] public void TestLocaleIdentifierGetter() { - ParseInstallation installation = ParseObjectExtensions.FromState(new MutableObjectState { ServerData = new Dictionary { ["localeIdentifier"] = "en-US" } }, "_Installation"); + ParseInstallation installation = Client.GenerateObjectFromState(new MutableObjectState { ServerData = new Dictionary { ["localeIdentifier"] = "en-US" } }, "_Installation"); + Assert.IsNotNull(installation); Assert.AreEqual("en-US", installation.LocaleIdentifier); } @@ -104,12 +118,14 @@ public void TestLocaleIdentifierGetter() [TestMethod] public void TestChannelGetterSetter() { - ParseInstallation installation = ParseObjectExtensions.FromState(new MutableObjectState { ServerData = new Dictionary { ["channels"] = new List { "the", "richard" } } }, "_Installation"); + ParseInstallation installation = Client.GenerateObjectFromState(new MutableObjectState { ServerData = new Dictionary { ["channels"] = new List { "the", "richard" } } }, "_Installation"); + Assert.IsNotNull(installation); Assert.AreEqual("the", installation.Channels[0]); Assert.AreEqual("richard", installation.Channels[1]); - installation.Channels = new List() { "mr", "kevin" }; + installation.Channels = new List { "mr", "kevin" }; + Assert.AreEqual("mr", installation.Channels[0]); Assert.AreEqual("kevin", installation.Channels[1]); } @@ -117,14 +133,20 @@ public void TestChannelGetterSetter() [TestMethod] public void TestGetCurrentInstallation() { + MutableServiceHub hub = new MutableServiceHub { }; + ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + Guid guid = Guid.NewGuid(); - ParseInstallation installation = ParseObjectExtensions.FromState(new MutableObjectState { ServerData = new Dictionary { ["installationId"] = guid.ToString() } }, "_Installation"); + + ParseInstallation installation = client.GenerateObjectFromState(new MutableObjectState { ServerData = new Dictionary { ["installationId"] = guid.ToString() } }, "_Installation"); + Mock mockController = new Mock(); - mockController.Setup(obj => obj.GetAsync(It.IsAny())).Returns(Task.FromResult(installation)); + mockController.Setup(obj => obj.GetAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(installation)); + + hub.CurrentInstallationController = mockController.Object; - ParsePushPlugins.Instance = new ParsePushPlugins { CurrentInstallationController = mockController.Object }; + ParseInstallation currentInstallation = client.GetCurrentInstallation(); - ParseInstallation currentInstallation = ParseInstallation.CurrentInstallation; Assert.IsNotNull(currentInstallation); Assert.AreEqual(guid, currentInstallation.InstallationId); } diff --git a/Parse.Test/JsonTests.cs b/Parse.Tests/JsonTests.cs similarity index 65% rename from Parse.Test/JsonTests.cs rename to Parse.Tests/JsonTests.cs index 1105a7f5..ef67f6e8 100644 --- a/Parse.Test/JsonTests.cs +++ b/Parse.Tests/JsonTests.cs @@ -1,89 +1,88 @@ -using Parse.Common.Internal; using System; using System.Collections; using System.Collections.Generic; - using Microsoft.VisualStudio.TestTools.UnitTesting; +using Parse.Infrastructure.Utilities; -namespace Parse.Test +namespace Parse.Tests { [TestClass] public class JsonTests { [TestMethod] - public void TestEmptyJsonStringFail() => Assert.ThrowsException(() => Json.Parse("")); + public void TestEmptyJsonStringFail() => Assert.ThrowsException(() => JsonUtilities.Parse("")); [TestMethod] public void TestInvalidJsonStringAsRootFail() { - Assert.ThrowsException(() => Json.Parse("\n")); - Assert.ThrowsException(() => Json.Parse("a")); - Assert.ThrowsException(() => Json.Parse("abc")); - Assert.ThrowsException(() => Json.Parse("\u1234")); - Assert.ThrowsException(() => Json.Parse("\t")); - Assert.ThrowsException(() => Json.Parse("\t\n\r")); - Assert.ThrowsException(() => Json.Parse(" ")); - Assert.ThrowsException(() => Json.Parse("1234")); - Assert.ThrowsException(() => Json.Parse("1,3")); - Assert.ThrowsException(() => Json.Parse("{1")); - Assert.ThrowsException(() => Json.Parse("3}")); - Assert.ThrowsException(() => Json.Parse("}")); + Assert.ThrowsException(() => JsonUtilities.Parse("\n")); + Assert.ThrowsException(() => JsonUtilities.Parse("a")); + Assert.ThrowsException(() => JsonUtilities.Parse("abc")); + Assert.ThrowsException(() => JsonUtilities.Parse("\u1234")); + Assert.ThrowsException(() => JsonUtilities.Parse("\t")); + Assert.ThrowsException(() => JsonUtilities.Parse("\t\n\r")); + Assert.ThrowsException(() => JsonUtilities.Parse(" ")); + Assert.ThrowsException(() => JsonUtilities.Parse("1234")); + Assert.ThrowsException(() => JsonUtilities.Parse("1,3")); + Assert.ThrowsException(() => JsonUtilities.Parse("{1")); + Assert.ThrowsException(() => JsonUtilities.Parse("3}")); + Assert.ThrowsException(() => JsonUtilities.Parse("}")); } [TestMethod] - public void TestEmptyJsonObject() => Assert.IsTrue(Json.Parse("{}") is IDictionary); + public void TestEmptyJsonObject() => Assert.IsTrue(JsonUtilities.Parse("{}") is IDictionary); [TestMethod] - public void TestEmptyJsonArray() => Assert.IsTrue(Json.Parse("[]") is IList); + public void TestEmptyJsonArray() => Assert.IsTrue(JsonUtilities.Parse("[]") is IList); [TestMethod] public void TestOneJsonObject() { - Assert.ThrowsException(() => Json.Parse("{ 1 }")); - Assert.ThrowsException(() => Json.Parse("{ 1 : 1 }")); - Assert.ThrowsException(() => Json.Parse("{ 1 : \"abc\" }")); + Assert.ThrowsException(() => JsonUtilities.Parse("{ 1 }")); + Assert.ThrowsException(() => JsonUtilities.Parse("{ 1 : 1 }")); + Assert.ThrowsException(() => JsonUtilities.Parse("{ 1 : \"abc\" }")); - object parsed = Json.Parse("{\"abc\" : \"def\"}"); + object parsed = JsonUtilities.Parse("{\"abc\" : \"def\"}"); Assert.IsTrue(parsed is IDictionary); IDictionary parsedDict = parsed as IDictionary; Assert.AreEqual("def", parsedDict["abc"]); - parsed = Json.Parse("{\"abc\" : {} }"); + parsed = JsonUtilities.Parse("{\"abc\" : {} }"); Assert.IsTrue(parsed is IDictionary); parsedDict = parsed as IDictionary; Assert.IsTrue(parsedDict["abc"] is IDictionary); - parsed = Json.Parse("{\"abc\" : \"6060\"}"); + parsed = JsonUtilities.Parse("{\"abc\" : \"6060\"}"); Assert.IsTrue(parsed is IDictionary); parsedDict = parsed as IDictionary; Assert.AreEqual("6060", parsedDict["abc"]); - parsed = Json.Parse("{\"\" : \"\"}"); + parsed = JsonUtilities.Parse("{\"\" : \"\"}"); Assert.IsTrue(parsed is IDictionary); parsedDict = parsed as IDictionary; Assert.AreEqual("", parsedDict[""]); - parsed = Json.Parse("{\" abc\" : \"def \"}"); + parsed = JsonUtilities.Parse("{\" abc\" : \"def \"}"); Assert.IsTrue(parsed is IDictionary); parsedDict = parsed as IDictionary; Assert.AreEqual("def ", parsedDict[" abc"]); - parsed = Json.Parse("{\"1\" : 6060}"); + parsed = JsonUtilities.Parse("{\"1\" : 6060}"); Assert.IsTrue(parsed is IDictionary); parsedDict = parsed as IDictionary; Assert.AreEqual((long) 6060, parsedDict["1"]); - parsed = Json.Parse("{\"1\" : null}"); + parsed = JsonUtilities.Parse("{\"1\" : null}"); Assert.IsTrue(parsed is IDictionary); parsedDict = parsed as IDictionary; Assert.IsNull(parsedDict["1"]); - parsed = Json.Parse("{\"1\" : true}"); + parsed = JsonUtilities.Parse("{\"1\" : true}"); Assert.IsTrue(parsed is IDictionary); parsedDict = parsed as IDictionary; Assert.IsTrue((bool) parsedDict["1"]); - parsed = Json.Parse("{\"1\" : false}"); + parsed = JsonUtilities.Parse("{\"1\" : false}"); Assert.IsTrue(parsed is IDictionary); parsedDict = parsed as IDictionary; Assert.IsFalse((bool) parsedDict["1"]); @@ -92,37 +91,37 @@ public void TestOneJsonObject() [TestMethod] public void TestMultipleJsonObjectAsRootFail() { - Assert.ThrowsException(() => Json.Parse("{},")); - Assert.ThrowsException(() => Json.Parse("{\"abc\" : \"def\"},")); - Assert.ThrowsException(() => Json.Parse("{\"abc\" : \"def\" \"def\"}")); - Assert.ThrowsException(() => Json.Parse("{}, {}")); - Assert.ThrowsException(() => Json.Parse("{},\n{}")); + Assert.ThrowsException(() => JsonUtilities.Parse("{},")); + Assert.ThrowsException(() => JsonUtilities.Parse("{\"abc\" : \"def\"},")); + Assert.ThrowsException(() => JsonUtilities.Parse("{\"abc\" : \"def\" \"def\"}")); + Assert.ThrowsException(() => JsonUtilities.Parse("{}, {}")); + Assert.ThrowsException(() => JsonUtilities.Parse("{},\n{}")); } [TestMethod] public void TestOneJsonArray() { - Assert.ThrowsException(() => Json.Parse("[ 1 : 1 ]")); - Assert.ThrowsException(() => Json.Parse("[ 1 1 ]")); - Assert.ThrowsException(() => Json.Parse("[ 1 : \"1\" ]")); - Assert.ThrowsException(() => Json.Parse("[ \"1\" : \"1\" ]")); + Assert.ThrowsException(() => JsonUtilities.Parse("[ 1 : 1 ]")); + Assert.ThrowsException(() => JsonUtilities.Parse("[ 1 1 ]")); + Assert.ThrowsException(() => JsonUtilities.Parse("[ 1 : \"1\" ]")); + Assert.ThrowsException(() => JsonUtilities.Parse("[ \"1\" : \"1\" ]")); - object parsed = Json.Parse("[ 1 ]"); + object parsed = JsonUtilities.Parse("[ 1 ]"); Assert.IsTrue(parsed is IList); IList parsedList = parsed as IList; Assert.AreEqual((long) 1, parsedList[0]); - parsed = Json.Parse("[ \n ]"); + parsed = JsonUtilities.Parse("[ \n ]"); Assert.IsTrue(parsed is IList); parsedList = parsed as IList; Assert.AreEqual(0, parsedList.Count); - parsed = Json.Parse("[ \"asdf\" ]"); + parsed = JsonUtilities.Parse("[ \"asdf\" ]"); Assert.IsTrue(parsed is IList); parsedList = parsed as IList; Assert.AreEqual("asdf", parsedList[0]); - parsed = Json.Parse("[ \"\u849c\" ]"); + parsed = JsonUtilities.Parse("[ \"\u849c\" ]"); Assert.IsTrue(parsed is IList); parsedList = parsed as IList; Assert.AreEqual("\u849c", parsedList[0]); @@ -131,25 +130,25 @@ public void TestOneJsonArray() [TestMethod] public void TestMultipleJsonArrayAsRootFail() { - Assert.ThrowsException(() => Json.Parse("[],")); - Assert.ThrowsException(() => Json.Parse("[\"abc\" : \"def\"],")); - Assert.ThrowsException(() => Json.Parse("[], []")); - Assert.ThrowsException(() => Json.Parse("[],\n[]")); + Assert.ThrowsException(() => JsonUtilities.Parse("[],")); + Assert.ThrowsException(() => JsonUtilities.Parse("[\"abc\" : \"def\"],")); + Assert.ThrowsException(() => JsonUtilities.Parse("[], []")); + Assert.ThrowsException(() => JsonUtilities.Parse("[],\n[]")); } [TestMethod] public void TestJsonArrayInsideJsonObject() { - Assert.ThrowsException(() => Json.Parse("{ [] }")); - Assert.ThrowsException(() => Json.Parse("{ [], [] }")); - Assert.ThrowsException(() => Json.Parse("{ \"abc\": [], [] }")); + Assert.ThrowsException(() => JsonUtilities.Parse("{ [] }")); + Assert.ThrowsException(() => JsonUtilities.Parse("{ [], [] }")); + Assert.ThrowsException(() => JsonUtilities.Parse("{ \"abc\": [], [] }")); - object parsed = Json.Parse("{ \"abc\": [] }"); + object parsed = JsonUtilities.Parse("{ \"abc\": [] }"); Assert.IsTrue(parsed is IDictionary); IDictionary parsedDict = parsed as IDictionary; Assert.IsTrue(parsedDict["abc"] is IList); - parsed = Json.Parse("{ \"6060\" :\n[ 6060 ]\t}"); + parsed = JsonUtilities.Parse("{ \"6060\" :\n[ 6060 ]\t}"); Assert.IsTrue(parsed is IDictionary); parsedDict = parsed as IDictionary; Assert.IsTrue(parsedDict["6060"] is IList); @@ -160,15 +159,15 @@ public void TestJsonArrayInsideJsonObject() [TestMethod] public void TestJsonObjectInsideJsonArray() { - Assert.ThrowsException(() => Json.Parse("[ {} : {} ]")); + Assert.ThrowsException(() => JsonUtilities.Parse("[ {} : {} ]")); // whitespace test - object parsed = Json.Parse("[\t\n{}\r\t]"); + object parsed = JsonUtilities.Parse("[\t\n{}\r\t]"); Assert.IsTrue(parsed is IList); IList parsedList = parsed as IList; Assert.IsTrue(parsedList[0] is IDictionary); - parsed = Json.Parse("[ {}, { \"final\" : \"fantasy\"} ]"); + parsed = JsonUtilities.Parse("[ {}, { \"final\" : \"fantasy\"} ]"); Assert.IsTrue(parsed is IList); parsedList = parsed as IList; Assert.IsTrue(parsedList[0] is IDictionary); @@ -182,11 +181,11 @@ public void TestJsonObjectWithElements() { // Just make sure they don't throw exception as we already check their content correctness // in other unit tests. - Json.Parse("{ \"mura\": \"masa\" }"); - Json.Parse("{ \"mura\": 1234 }"); - Json.Parse("{ \"mura\": { \"masa\": 1234 } }"); - Json.Parse("{ \"mura\": { \"masa\": [ 1234 ] } }"); - Json.Parse("{ \"mura\": { \"masa\": [ 1234 ] }, \"arr\": [] }"); + JsonUtilities.Parse("{ \"mura\": \"masa\" }"); + JsonUtilities.Parse("{ \"mura\": 1234 }"); + JsonUtilities.Parse("{ \"mura\": { \"masa\": 1234 } }"); + JsonUtilities.Parse("{ \"mura\": { \"masa\": [ 1234 ] } }"); + JsonUtilities.Parse("{ \"mura\": { \"masa\": [ 1234 ] }, \"arr\": [] }"); } [TestMethod] @@ -194,59 +193,62 @@ public void TestJsonArrayWithElements() { // Just make sure they don't throw exception as we already check their content correctness // in other unit tests. - Json.Parse("[ \"mura\" ]"); - Json.Parse("[ \"\u1234\" ]"); - Json.Parse("[ \"\u1234ff\", \"\u1234\" ]"); - Json.Parse("[ [], [], [], [] ]"); - Json.Parse("[ [], [ {}, {} ], [ {} ], [] ]"); + JsonUtilities.Parse("[ \"mura\" ]"); + JsonUtilities.Parse("[ \"\u1234\" ]"); + JsonUtilities.Parse("[ \"\u1234ff\", \"\u1234\" ]"); + JsonUtilities.Parse("[ [], [], [], [] ]"); + JsonUtilities.Parse("[ [], [ {}, {} ], [ {} ], [] ]"); } [TestMethod] public void TestEncodeJson() { Dictionary dict = new Dictionary(); - string encoded = Json.Encode(dict); + string encoded = JsonUtilities.Encode(dict); Assert.AreEqual("{}", encoded); List list = new List(); - encoded = Json.Encode(list); + encoded = JsonUtilities.Encode(list); Assert.AreEqual("[]", encoded); Dictionary dictChild = new Dictionary(); list.Add(dictChild); - encoded = Json.Encode(list); + encoded = JsonUtilities.Encode(list); Assert.AreEqual("[{}]", encoded); list.Add("1234 a\t\r\n"); list.Add(1234); list.Add(12.34); list.Add(1.23456789123456789); - encoded = Json.Encode(list); - Assert.AreEqual("[{},\"1234 a\\t\\r\\n\",1234,12.34,1.23456789123457]", encoded); + encoded = JsonUtilities.Encode(list); + + // This string should be [{},\"1234 a\\t\\r\\n\",1234,12.34,1.23456789123457] for .NET Framework (https://github.com/dotnet/runtime/issues/31483). + + Assert.AreEqual("[{},\"1234 a\\t\\r\\n\",1234,12.34,1.234567891234568]", encoded); dict["arr"] = new List(); - encoded = Json.Encode(dict); + encoded = JsonUtilities.Encode(dict); Assert.AreEqual("{\"arr\":[]}", encoded); dict["\u1234"] = "\u1234"; - encoded = Json.Encode(dict); + encoded = JsonUtilities.Encode(dict); Assert.AreEqual("{\"arr\":[],\"\u1234\":\"\u1234\"}", encoded); - encoded = Json.Encode(new List { true, false, null }); + encoded = JsonUtilities.Encode(new List { true, false, null }); Assert.AreEqual("[true,false,null]", encoded); } [TestMethod] public void TestSpecialJsonNumbersAndModifiers() { - Assert.ThrowsException(() => Json.Parse("+123456789")); + Assert.ThrowsException(() => JsonUtilities.Parse("+123456789")); - Json.Parse("{ \"mura\": -123456789123456789 }"); - Json.Parse("{ \"mura\": 1.1234567891234567E308 }"); - Json.Parse("{ \"PI\": 3.141e-10 }"); - Json.Parse("{ \"PI\": 3.141E-10 }"); + JsonUtilities.Parse("{ \"mura\": -123456789123456789 }"); + JsonUtilities.Parse("{ \"mura\": 1.1234567891234567E308 }"); + JsonUtilities.Parse("{ \"PI\": 3.141e-10 }"); + JsonUtilities.Parse("{ \"PI\": 3.141E-10 }"); - Assert.AreEqual(123456789123456789, (Json.Parse("{ \"mura\": 123456789123456789 }") as IDictionary)["mura"]); + Assert.AreEqual(123456789123456789, (JsonUtilities.Parse("{ \"mura\": 123456789123456789 }") as IDictionary)["mura"]); } } } diff --git a/Parse.Tests/LateInitializerTests.cs b/Parse.Tests/LateInitializerTests.cs new file mode 100644 index 00000000..e4e51436 --- /dev/null +++ b/Parse.Tests/LateInitializerTests.cs @@ -0,0 +1,40 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Parse.Infrastructure.Utilities; + +namespace Parse.Tests +{ + // TODO: Add more tests. + + [TestClass] + public class LateInitializerTests + { + LateInitializer LateInitializer { get; } = new LateInitializer { }; + + [TestInitialize] + public void Clear() => LateInitializer.Reset(); + + [DataTestMethod, DataRow("Bruh", "Hello"), DataRow("Cheese", ""), DataRow("", "Waffle"), DataRow("Toaster", "Toad"), DataRow(default, "Duck"), DataRow("Dork", default)] + public void TestAlteredValueGetValuePostGenerationCall(string initialValue, string finalValue) + { + string GetValue() => LateInitializer.GetValue(() => initialValue); + bool SetValue() => LateInitializer.SetValue(finalValue); + + Assert.AreEqual(initialValue, GetValue()); + + Assert.IsTrue(SetValue()); + Assert.AreNotEqual(initialValue, GetValue()); + Assert.AreEqual(finalValue, GetValue()); + } + + [DataTestMethod, DataRow("Bruh", "Hello"), DataRow("Cheese", ""), DataRow("", "Waffle"), DataRow("Toaster", "Toad"), DataRow(default, "Duck"), DataRow("Dork", default)] + public void TestInitialGetValueCallPostSetValueCall(string initialValue, string finalValue) + { + string GetValue() => LateInitializer.GetValue(() => finalValue); + bool SetValue() => LateInitializer.SetValue(initialValue); + + Assert.IsTrue(SetValue()); + Assert.AreNotEqual(finalValue, GetValue()); + Assert.AreEqual(initialValue, GetValue()); + } + } +} diff --git a/Parse.Test/MoqExtensions.cs b/Parse.Tests/MoqExtensions.cs similarity index 94% rename from Parse.Test/MoqExtensions.cs rename to Parse.Tests/MoqExtensions.cs index f9569e79..abd9a606 100644 --- a/Parse.Test/MoqExtensions.cs +++ b/Parse.Tests/MoqExtensions.cs @@ -1,8 +1,8 @@ -using Moq.Language; +using System.Reflection; +using Moq.Language; using Moq.Language.Flow; -using System.Reflection; -namespace Parse.Test +namespace Parse.Tests { // MIT licensed, w/ attribution: // http://stackoverflow.com/a/19598345/427309 diff --git a/Parse.Test/ObjectCoderTests.cs b/Parse.Tests/ObjectCoderTests.cs similarity index 82% rename from Parse.Test/ObjectCoderTests.cs rename to Parse.Tests/ObjectCoderTests.cs index 488f8216..fb2d0e45 100644 --- a/Parse.Test/ObjectCoderTests.cs +++ b/Parse.Tests/ObjectCoderTests.cs @@ -1,16 +1,10 @@ -using System.Xml.XPath; -using System.Linq; -using System.Diagnostics; -using Castle.DynamicProxy.Generators.Emitters; -using Parse.Core.Internal; -using System; using System.Collections.Generic; -using System.Text; -using Parse; - using Microsoft.VisualStudio.TestTools.UnitTesting; +using Parse.Infrastructure; +using Parse.Infrastructure.Data; +using Parse.Platform.Objects; -namespace Parse.Test +namespace Parse.Tests { [TestClass] public class ObjectCoderTests @@ -29,9 +23,10 @@ public void TestACLCoding() }, ["*"] = new Dictionary { ["read"] = true } } - }, null); + }, default, new ServiceHub { }); + + ParseACL resultACL = default; - ParseACL resultACL = null; Assert.IsTrue(state.ContainsKey("ACL")); Assert.IsTrue((resultACL = state.ServerData["ACL"] as ParseACL) is ParseACL); Assert.IsTrue(resultACL.PublicReadAccess); diff --git a/Parse.Test/ObjectControllerTests.cs b/Parse.Tests/ObjectControllerTests.cs similarity index 69% rename from Parse.Test/ObjectControllerTests.cs rename to Parse.Tests/ObjectControllerTests.cs index 3ed4f4b9..d56da4da 100644 --- a/Parse.Test/ObjectControllerTests.cs +++ b/Parse.Tests/ObjectControllerTests.cs @@ -1,6 +1,3 @@ -using Moq; -using Parse; -using Parse.Core.Internal; using System; using System.Collections.Generic; using System.Linq; @@ -8,16 +5,27 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Parse.Test +using Moq; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Control; +using Parse.Abstractions.Infrastructure.Execution; +using Parse.Abstractions.Platform.Objects; +using Parse.Infrastructure; +using Parse.Infrastructure.Execution; +using Parse.Platform.Objects; + +namespace Parse.Tests { +#warning Finish refactoring. + [TestClass] public class ObjectControllerTests { + ParseClient Client { get; set; } + [TestInitialize] - public void SetUp() => ParseClient.Initialize(new ParseClient.Configuration { ApplicationID = "", Key = "" }); + public void SetUp() => Client = new ParseClient(new ServerConnectionData { ApplicationID = "", Key = "", Test = true }); [TestMethod] [AsyncStateMachine(typeof(ObjectControllerTests))] @@ -25,14 +33,14 @@ public Task TestFetch() { Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, new Dictionary { ["__type"] = "Object", ["className"] = "Corgi", ["objectId"] = "st4nl3yW", ["doge"] = "isShibaInu", ["createdAt"] = "2015-09-18T18:11:28.943Z" })); - return new ParseObjectController(mockRunner.Object).FetchAsync(new MutableObjectState { ClassName = "Corgi", ObjectId = "st4nl3yW", ServerData = new Dictionary { ["corgi"] = "isNotDoge" } }, null, CancellationToken.None).ContinueWith(t => + return new ParseObjectController(mockRunner.Object, Client.Decoder, Client.ServerConnectionData).FetchAsync(new MutableObjectState { ClassName = "Corgi", ObjectId = "st4nl3yW", ServerData = new Dictionary { ["corgi"] = "isNotDoge" } }, default, Client, CancellationToken.None).ContinueWith(task => { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Uri.AbsolutePath == "/1/classes/Corgi/st4nl3yW"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); + mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "classes/Corgi/st4nl3yW"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); - IObjectState newState = t.Result; + IObjectState newState = task.Result; Assert.AreEqual("isShibaInu", newState["doge"]); Assert.IsFalse(newState.ContainsKey("corgi")); Assert.IsNotNull(newState.CreatedAt); @@ -46,14 +54,14 @@ public Task TestSave() { Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, new Dictionary { ["__type"] = "Object", ["className"] = "Corgi", ["objectId"] = "st4nl3yW", ["doge"] = "isShibaInu", ["createdAt"] = "2015-09-18T18:11:28.943Z" })); - return new ParseObjectController(mockRunner.Object).SaveAsync(new MutableObjectState { ClassName = "Corgi", ObjectId = "st4nl3yW", ServerData = new Dictionary { ["corgi"] = "isNotDoge" } }, new Dictionary { ["gogo"] = new Mock { }.Object }, null, CancellationToken.None).ContinueWith(t => + return new ParseObjectController(mockRunner.Object, Client.Decoder, Client.ServerConnectionData).SaveAsync(new MutableObjectState { ClassName = "Corgi", ObjectId = "st4nl3yW", ServerData = new Dictionary { ["corgi"] = "isNotDoge" } }, new Dictionary { ["gogo"] = new Mock { }.Object }, default, Client, CancellationToken.None).ContinueWith(task => { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Uri.AbsolutePath == "/1/classes/Corgi/st4nl3yW"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); + mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "classes/Corgi/st4nl3yW"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); - IObjectState newState = t.Result; + IObjectState newState = task.Result; Assert.AreEqual("isShibaInu", newState["doge"]); Assert.IsFalse(newState.ContainsKey("corgi")); Assert.IsFalse(newState.ContainsKey("gogo")); @@ -84,18 +92,15 @@ public Task TestSaveNewObject() Tuple> response = new Tuple>(HttpStatusCode.Created, responseDict); Mock mockRunner = CreateMockRunner(response); - ParseObjectController controller = new ParseObjectController(mockRunner.Object); - return controller.SaveAsync(state, operations, null, CancellationToken.None).ContinueWith(t => + ParseObjectController controller = new ParseObjectController(mockRunner.Object, Client.Decoder, Client.ServerConnectionData); + return controller.SaveAsync(state, operations, default, Client, CancellationToken.None).ContinueWith(task => { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Uri.AbsolutePath == "/1/classes/Corgi"), - It.IsAny>(), - It.IsAny>(), - It.IsAny()), Times.Exactly(1)); + mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "classes/Corgi"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); - IObjectState newState = t.Result; + IObjectState newState = task.Result; Assert.AreEqual("isShibaInu", newState["doge"]); Assert.IsFalse(newState.ContainsKey("corgi")); Assert.IsFalse(newState.ContainsKey("gogo")); @@ -111,20 +116,26 @@ public Task TestSaveNewObject() public Task TestSaveAll() { List states = new List(); + for (int i = 0; i < 30; ++i) { states.Add(new MutableObjectState { ClassName = "Corgi", - ObjectId = ((i % 2 == 0) ? null : "st4nl3yW" + i), + ObjectId = (i % 2 == 0) ? null : "st4nl3yW" + i, ServerData = new Dictionary { ["corgi"] = "isNotDoge" } }); } + List> operationsList = new List>(); + for (int i = 0; i < 30; ++i) + { operationsList.Add(new Dictionary { ["gogo"] = new Mock { }.Object }); + } List> results = new List>(); + for (int i = 0; i < 30; ++i) { results.Add(new Dictionary @@ -139,13 +150,14 @@ public Task TestSaveAll() } }); } - Dictionary responseDict = new Dictionary { ["results"] = results }; + + Dictionary responseDict = new Dictionary { [nameof(results)] = results }; Tuple> response = new Tuple>(HttpStatusCode.OK, responseDict); Mock mockRunner = CreateMockRunner(response); - ParseObjectController controller = new ParseObjectController(mockRunner.Object); - IList> tasks = controller.SaveAllAsync(states, operationsList, null, CancellationToken.None); + ParseObjectController controller = new ParseObjectController(mockRunner.Object, Client.Decoder, Client.ServerConnectionData); + IList> tasks = controller.SaveAllAsync(states, operationsList, default, Client, CancellationToken.None); return Task.WhenAll(tasks).ContinueWith(_ => { @@ -162,10 +174,7 @@ public Task TestSaveAll() Assert.IsNotNull(serverState.UpdatedAt); } - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Uri.AbsolutePath == "/1/batch"), - It.IsAny>(), - It.IsAny>(), - It.IsAny()), Times.Exactly(1)); + mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "batch"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); }); } @@ -180,7 +189,10 @@ public Task TestSaveAllManyObjects() { ClassName = "Corgi", ObjectId = "st4nl3yW" + i, - ServerData = new Dictionary { ["corgi"] = "isNotDoge" } + ServerData = new Dictionary + { + ["corgi"] = "isNotDoge" + } }); } List> operationsList = new List>(); @@ -204,7 +216,7 @@ public Task TestSaveAllManyObjects() } }); } - Dictionary responseDict = new Dictionary { ["results"] = results }; + Dictionary responseDict = new Dictionary { [nameof(results)] = results }; Tuple> response = new Tuple>(HttpStatusCode.OK, responseDict); List> results2 = new List>(); @@ -222,14 +234,14 @@ public Task TestSaveAllManyObjects() } }); } - Dictionary responseDict2 = new Dictionary { ["results"] = results2 }; + Dictionary responseDict2 = new Dictionary { [nameof(results)] = results2 }; Tuple> response2 = new Tuple>(HttpStatusCode.OK, responseDict2); Mock mockRunner = new Mock { }; - mockRunner.SetupSequence(obj => obj.RunCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(response)).Returns(Task.FromResult(response)).Returns(Task.FromResult(response2)); + mockRunner.SetupSequence(obj => obj.RunCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(response)).Returns(Task.FromResult(response)).Returns(Task.FromResult(response2)); - ParseObjectController controller = new ParseObjectController(mockRunner.Object); - IList> tasks = controller.SaveAllAsync(states, operationsList, null, CancellationToken.None); + ParseObjectController controller = new ParseObjectController(mockRunner.Object, Client.Decoder, Client.ServerConnectionData); + IList> tasks = controller.SaveAllAsync(states, operationsList, default, Client, CancellationToken.None); return Task.WhenAll(tasks).ContinueWith(_ => { @@ -238,7 +250,7 @@ public Task TestSaveAllManyObjects() for (int i = 0; i < 102; ++i) { IObjectState serverState = tasks[i].Result; - Assert.AreEqual("st4nl3yW" + (i % 50), serverState.ObjectId); + Assert.AreEqual("st4nl3yW" + i % 50, serverState.ObjectId); Assert.IsFalse(serverState.ContainsKey("gogo")); Assert.IsFalse(serverState.ContainsKey("corgi")); Assert.AreEqual("isShibaInu", serverState["doge"]); @@ -246,7 +258,7 @@ public Task TestSaveAllManyObjects() Assert.IsNotNull(serverState.UpdatedAt); } - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Uri.AbsolutePath == "/1/batch"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(3)); + mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "batch"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(3)); }); } @@ -261,19 +273,14 @@ public Task TestDelete() ServerData = new Dictionary { ["corgi"] = "isNotDoge" } }; - Tuple> response = new Tuple>(HttpStatusCode.OK, new Dictionary()); - Mock mockRunner = CreateMockRunner(response); + Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.OK, new Dictionary { })); - ParseObjectController controller = new ParseObjectController(mockRunner.Object); - return controller.DeleteAsync(state, null, CancellationToken.None).ContinueWith(t => + return new ParseObjectController(mockRunner.Object, Client.Decoder, Client.ServerConnectionData).DeleteAsync(state, default, CancellationToken.None).ContinueWith(task => { - Assert.IsFalse(t.IsFaulted); - Assert.IsFalse(t.IsCanceled); + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Uri.AbsolutePath == "/1/classes/Corgi/st4nl3yW"), - It.IsAny>(), - It.IsAny>(), - It.IsAny()), Times.Exactly(1)); + mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "classes/Corgi/st4nl3yW"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); }); } @@ -295,21 +302,23 @@ public Task TestDeleteAll() List> results = new List>(); for (int i = 0; i < 30; ++i) + { results.Add(new Dictionary { ["success"] = null }); + } - Dictionary responseDict = new Dictionary { ["results"] = results }; + Dictionary responseDict = new Dictionary { [nameof(results)] = results }; Tuple> response = new Tuple>(HttpStatusCode.OK, responseDict); Mock mockRunner = CreateMockRunner(response); - ParseObjectController controller = new ParseObjectController(mockRunner.Object); - IList tasks = controller.DeleteAllAsync(states, null, CancellationToken.None); + ParseObjectController controller = new ParseObjectController(mockRunner.Object, Client.Decoder, Client.ServerConnectionData); + IList tasks = controller.DeleteAllAsync(states, default, CancellationToken.None); return Task.WhenAll(tasks).ContinueWith(_ => { Assert.IsTrue(tasks.All(task => task.IsCompleted && !task.IsCanceled && !task.IsFaulted)); - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Uri.AbsolutePath == "/1/batch"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); + mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "batch"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); }); } @@ -328,34 +337,39 @@ public Task TestDeleteAllManyObjects() }); } - // Make multiple response since the batch will be splitted. + // Make multiple response since the batch will be split. + List> results = new List>(); for (int i = 0; i < 50; ++i) - results.Add(new Dictionary { ["success"] = null }); + { + results.Add(new Dictionary { ["success"] = default }); + } - Dictionary responseDict = new Dictionary { ["results"] = results }; + Dictionary responseDict = new Dictionary { [nameof(results)] = results }; Tuple> response = new Tuple>(HttpStatusCode.OK, responseDict); List> results2 = new List>(); for (int i = 0; i < 2; ++i) - results2.Add(new Dictionary { ["success"] = null }); + { + results2.Add(new Dictionary { ["success"] = default }); + } - Dictionary responseDict2 = new Dictionary { ["results"] = results2 }; + Dictionary responseDict2 = new Dictionary { [nameof(results)] = results2 }; Tuple> response2 = new Tuple>(HttpStatusCode.OK, responseDict2); Mock mockRunner = new Mock(); - mockRunner.SetupSequence(obj => obj.RunCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(response)).Returns(Task.FromResult(response)).Returns(Task.FromResult(response2)); + mockRunner.SetupSequence(obj => obj.RunCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(response)).Returns(Task.FromResult(response)).Returns(Task.FromResult(response2)); - ParseObjectController controller = new ParseObjectController(mockRunner.Object); + ParseObjectController controller = new ParseObjectController(mockRunner.Object, Client.Decoder, Client.ServerConnectionData); IList tasks = controller.DeleteAllAsync(states, null, CancellationToken.None); return Task.WhenAll(tasks).ContinueWith(_ => { Assert.IsTrue(tasks.All(task => task.IsCompleted && !task.IsCanceled && !task.IsFaulted)); - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Uri.AbsolutePath == "/1/batch"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(3)); + mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "batch"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(3)); }); } @@ -363,18 +377,19 @@ public Task TestDeleteAllManyObjects() [AsyncStateMachine(typeof(ObjectControllerTests))] public Task TestDeleteAllFailSome() { - List states = new List(); + List states = new List { }; + for (int i = 0; i < 30; ++i) { states.Add(new MutableObjectState { ClassName = "Corgi", - ObjectId = ((i % 2 == 0) ? null : "st4nl3yW" + i), + ObjectId = (i % 2 == 0) ? null : "st4nl3yW" + i, ServerData = new Dictionary { ["corgi"] = "isNotDoge" } }); } - List> results = new List>(); + List> results = new List> { }; for (int i = 0; i < 15; ++i) { @@ -384,20 +399,21 @@ public Task TestDeleteAllFailSome() { ["error"] = new Dictionary { - ["code"] = (long) ParseException.ErrorCode.ObjectNotFound, + ["code"] = (long) ParseFailureException.ErrorCode.ObjectNotFound, ["error"] = "Object not found." } }); } - else results.Add(new Dictionary { ["success"] = null }); + else + { + results.Add(new Dictionary { ["success"] = default }); + } } - Dictionary responseDict = new Dictionary { ["results"] = results }; - Tuple> response = new Tuple>(HttpStatusCode.OK, responseDict); - Mock mockRunner = CreateMockRunner(response); + Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.OK, new Dictionary { [nameof(results)] = results })); - ParseObjectController controller = new ParseObjectController(mockRunner.Object); + ParseObjectController controller = new ParseObjectController(mockRunner.Object, Client.Decoder, Client.ServerConnectionData); IList tasks = controller.DeleteAllAsync(states, null, CancellationToken.None); return Task.WhenAll(tasks).ContinueWith(_ => @@ -407,9 +423,9 @@ public Task TestDeleteAllFailSome() if (i % 2 == 0) { Assert.IsTrue(tasks[i].IsFaulted); - Assert.IsInstanceOfType(tasks[i].Exception.InnerException, typeof(ParseException)); - ParseException exception = tasks[i].Exception.InnerException as ParseException; - Assert.AreEqual(ParseException.ErrorCode.ObjectNotFound, exception.Code); + Assert.IsInstanceOfType(tasks[i].Exception.InnerException, typeof(ParseFailureException)); + ParseFailureException exception = tasks[i].Exception.InnerException as ParseFailureException; + Assert.AreEqual(ParseFailureException.ErrorCode.ObjectNotFound, exception.Code); } else { @@ -418,10 +434,7 @@ public Task TestDeleteAllFailSome() } } - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Uri.AbsolutePath == "/1/batch"), - It.IsAny>(), - It.IsAny>(), - It.IsAny()), Times.Exactly(1)); + mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "batch"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); }); } @@ -429,34 +442,31 @@ public Task TestDeleteAllFailSome() [AsyncStateMachine(typeof(ObjectControllerTests))] public Task TestDeleteAllInconsistent() { - List states = new List(); + List states = new List { }; + for (int i = 0; i < 30; ++i) { states.Add(new MutableObjectState { ClassName = "Corgi", ObjectId = "st4nl3yW" + i, - ServerData = new Dictionary() { - { "corgi", "isNotDoge" }, - } + ServerData = new Dictionary + { + ["corgi"] = "isNotDoge" + } }); } - List> results = new List>(); + List> results = new List> { }; + for (int i = 0; i < 36; ++i) { - results.Add(new Dictionary() { - { "success", null } - }); + results.Add(new Dictionary { ["success"] = default }); } - Dictionary responseDict = new Dictionary() { - { "results", results } - }; - Tuple> response = new Tuple>(HttpStatusCode.OK, responseDict); - Mock mockRunner = CreateMockRunner(response); + Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.OK, new Dictionary { [nameof(results)] = results })); - ParseObjectController controller = new ParseObjectController(mockRunner.Object); + ParseObjectController controller = new ParseObjectController(mockRunner.Object, Client.Decoder, Client.ServerConnectionData); IList tasks = controller.DeleteAllAsync(states, null, CancellationToken.None); return Task.WhenAll(tasks).ContinueWith(_ => @@ -464,21 +474,14 @@ public Task TestDeleteAllInconsistent() Assert.IsTrue(tasks.All(task => task.IsFaulted)); Assert.IsInstanceOfType(tasks[0].Exception.InnerException, typeof(InvalidOperationException)); - mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Uri.AbsolutePath == "/1/batch"), - It.IsAny>(), - It.IsAny>(), - It.IsAny()), Times.Exactly(1)); + mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "batch"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); }); } private Mock CreateMockRunner(Tuple> response) { Mock mockRunner = new Mock(); - mockRunner.Setup(obj => obj.RunCommandAsync(It.IsAny(), - It.IsAny>(), - It.IsAny>(), - It.IsAny())) - .Returns(Task.FromResult(response)); + mockRunner.Setup(obj => obj.RunCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(response)); return mockRunner; } diff --git a/Parse.Test/ObjectStateTests.cs b/Parse.Tests/ObjectStateTests.cs similarity index 93% rename from Parse.Test/ObjectStateTests.cs rename to Parse.Tests/ObjectStateTests.cs index 65d5cbb3..250ce4ba 100644 --- a/Parse.Test/ObjectStateTests.cs +++ b/Parse.Tests/ObjectStateTests.cs @@ -1,11 +1,13 @@ -using Parse.Core.Internal; using System; using System.Collections.Generic; using System.Linq; - using Microsoft.VisualStudio.TestTools.UnitTesting; +using Parse.Abstractions.Infrastructure.Control; +using Parse.Abstractions.Platform.Objects; +using Parse.Infrastructure.Control; +using Parse.Platform.Objects; -namespace Parse.Test +namespace Parse.Tests { [TestClass] public class ObjectStateTests @@ -92,10 +94,7 @@ public void TestApplyOperation() Assert.AreEqual(2, state["exist"]); Assert.AreEqual("teletubies", state["change"]); - state = state.MutatedClone(mutableClone => - { - mutableClone.Apply(operations); - }); + state = state.MutatedClone(mutableClone => mutableClone.Apply(operations)); Assert.AreEqual(3, state.Count()); Assert.AreEqual(9, state["exist"]); @@ -128,10 +127,7 @@ public void TestApplyState() } }; - state = state.MutatedClone(mutableClone => - { - mutableClone.Apply(appliedState); - }); + state = state.MutatedClone(mutableClone => mutableClone.Apply(appliedState)); Assert.AreEqual("Corgi", state.ClassName); Assert.AreEqual("1234", state.ObjectId); diff --git a/Parse.Test/ObjectTests.cs b/Parse.Tests/ObjectTests.cs similarity index 63% rename from Parse.Test/ObjectTests.cs rename to Parse.Tests/ObjectTests.cs index ac51ad45..e3aef397 100644 --- a/Parse.Test/ObjectTests.cs +++ b/Parse.Tests/ObjectTests.cs @@ -1,26 +1,29 @@ -using Parse; -using Parse.Core.Internal; using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; +using Parse.Abstractions.Internal; +using Parse.Abstractions.Platform.Objects; +using Parse.Infrastructure; +using Parse.Platform.Objects; -namespace Parse.Test +namespace Parse.Tests { [TestClass] public class ObjectTests { - [ParseClassName("SubClass")] - private class SubClass : ParseObject { } + [ParseClassName(nameof(SubClass))] + class SubClass : ParseObject { } + + [ParseClassName(nameof(UnregisteredSubClass))] + class UnregisteredSubClass : ParseObject { } - [ParseClassName("UnregisteredSubClass")] - private class UnregisteredSubClass : ParseObject { } + ParseClient Client { get; } = new ParseClient(new ServerConnectionData { Test = true }); [TestCleanup] - public void TearDown() => ParseCorePlugins.Instance.Reset(); + public void TearDown() => (Client.Services as ServiceHub).Reset(); [TestMethod] public void TestParseObjectConstructor() @@ -35,13 +38,13 @@ public void TestParseObjectConstructor() [TestMethod] public void TestParseObjectCreate() { - ParseObject obj = ParseObject.Create("Corgi"); + ParseObject obj = Client.CreateObject("Corgi"); Assert.AreEqual("Corgi", obj.ClassName); Assert.IsNull(obj.CreatedAt); Assert.IsTrue(obj.IsDataAvailable); Assert.IsTrue(obj.IsDirty); - ParseObject obj2 = ParseObject.CreateWithoutData("Corgi", "waGiManPutr4Pet1r"); + ParseObject obj2 = Client.CreateObjectWithoutData("Corgi", "waGiManPutr4Pet1r"); Assert.AreEqual("Corgi", obj2.ClassName); Assert.AreEqual("waGiManPutr4Pet1r", obj2.ObjectId); Assert.IsNull(obj2.CreatedAt); @@ -52,16 +55,16 @@ public void TestParseObjectCreate() [TestMethod] public void TestParseObjectCreateWithGeneric() { - ParseObject.RegisterSubclass(); + Client.AddValidClass(); - ParseObject obj = ParseObject.Create(); - Assert.AreEqual("SubClass", obj.ClassName); + ParseObject obj = Client.CreateObject(); + Assert.AreEqual(nameof(SubClass), obj.ClassName); Assert.IsNull(obj.CreatedAt); Assert.IsTrue(obj.IsDataAvailable); Assert.IsTrue(obj.IsDirty); - ParseObject obj2 = ParseObject.CreateWithoutData("waGiManPutr4Pet1r"); - Assert.AreEqual("SubClass", obj2.ClassName); + ParseObject obj2 = Client.CreateObjectWithoutData("waGiManPutr4Pet1r"); + Assert.AreEqual(nameof(SubClass), obj2.ClassName); Assert.AreEqual("waGiManPutr4Pet1r", obj2.ObjectId); Assert.IsNull(obj2.CreatedAt); Assert.IsFalse(obj2.IsDataAvailable); @@ -69,7 +72,7 @@ public void TestParseObjectCreateWithGeneric() } [TestMethod] - public void TestParseObjectCreateWithGenericFailWithoutSubclass() => Assert.ThrowsException(() => ParseObject.Create()); + public void TestParseObjectCreateWithGenericFailWithoutSubclass() => Assert.ThrowsException(() => Client.CreateObject()); [TestMethod] public void TestFromState() @@ -78,13 +81,15 @@ public void TestFromState() { ObjectId = "waGiManPutr4Pet1r", ClassName = "Pagi", - CreatedAt = new DateTime(), - ServerData = new Dictionary() { - { "username", "kevin" }, - { "sessionToken", "se551onT0k3n" } - } + CreatedAt = new DateTime { }, + ServerData = new Dictionary + { + ["username"] = "kevin", + ["sessionToken"] = "se551onT0k3n" + } }; - ParseObject obj = ParseObjectExtensions.FromState(state, "Omitted"); + + ParseObject obj = Client.GenerateObjectFromState(state, "Omitted"); Assert.AreEqual("waGiManPutr4Pet1r", obj.ObjectId); Assert.AreEqual("Pagi", obj.ClassName); @@ -97,65 +102,70 @@ public void TestFromState() [TestMethod] public void TestRegisterSubclass() { - Assert.ThrowsException(() => ParseObject.Create()); + Assert.ThrowsException(() => Client.CreateObject()); try { - ParseObject.RegisterSubclass(); - ParseObject.Create(); + Client.AddValidClass(); + Client.CreateObject(); - ParseCorePlugins.Instance.SubclassingController.UnregisterSubclass(typeof(UnregisteredSubClass)); - ParseObject.Create(); + Client.ClassController.RemoveClass(typeof(UnregisteredSubClass)); + Client.CreateObject(); } catch { Assert.Fail(); } - ParseCorePlugins.Instance.SubclassingController.UnregisterSubclass(typeof(SubClass)); - Assert.ThrowsException(() => ParseObject.Create()); + Client.ClassController.RemoveClass(typeof(SubClass)); + Assert.ThrowsException(() => Client.CreateObject()); } [TestMethod] public void TestRevert() { - ParseObject obj = ParseObject.Create("Corgi"); + ParseObject obj = Client.CreateObject("Corgi"); obj["gogo"] = true; Assert.IsTrue(obj.IsDirty); - Assert.AreEqual(1, obj.GetCurrentOperations().Count); + Assert.AreEqual(1, obj.CurrentOperations.Count); Assert.IsTrue(obj.ContainsKey("gogo")); obj.Revert(); Assert.IsTrue(obj.IsDirty); - Assert.AreEqual(0, obj.GetCurrentOperations().Count); + Assert.AreEqual(0, obj.CurrentOperations.Count); Assert.IsFalse(obj.ContainsKey("gogo")); } [TestMethod] public void TestDeepTraversal() { - ParseObject obj = ParseObject.Create("Corgi"); - IDictionary someDict = new Dictionary() { - { "someList", new List() } - }; - obj["obj"] = ParseObject.Create("Pug"); - obj["obj2"] = ParseObject.Create("Pug"); + ParseObject obj = Client.CreateObject("Corgi"); + + IDictionary someDict = new Dictionary + { + ["someList"] = new List { } + }; + + obj[nameof(obj)] = Client.CreateObject("Pug"); + obj["obj2"] = Client.CreateObject("Pug"); obj["list"] = new List(); obj["dict"] = someDict; obj["someBool"] = true; obj["someInt"] = 23; - IEnumerable traverseResult = ParseObjectExtensions.DeepTraversal(obj, true, true); + IEnumerable traverseResult = Client.TraverseObjectDeep(obj, true, true); Assert.AreEqual(8, traverseResult.Count()); - // Don't traverse beyond the root (since root is ParseObject) - traverseResult = ParseObjectExtensions.DeepTraversal(obj, false, true); + // Don't traverse beyond the root (since root is ParseObject). + + traverseResult = Client.TraverseObjectDeep(obj, false, true); Assert.AreEqual(1, traverseResult.Count()); - traverseResult = ParseObjectExtensions.DeepTraversal(someDict, false, true); + traverseResult = Client.TraverseObjectDeep(someDict, false, true); Assert.AreEqual(2, traverseResult.Count()); - // Should ignore root - traverseResult = ParseObjectExtensions.DeepTraversal(obj, true, false); + // Should ignore root. + + traverseResult = Client.TraverseObjectDeep(obj, true, false); Assert.AreEqual(7, traverseResult.Count()); } @@ -163,7 +173,7 @@ public void TestDeepTraversal() [TestMethod] public void TestRemove() { - ParseObject obj = ParseObject.Create("Corgi"); + ParseObject obj = Client.CreateObject("Corgi"); obj["gogo"] = true; Assert.IsTrue(obj.ContainsKey("gogo")); @@ -174,14 +184,15 @@ public void TestRemove() { ObjectId = "waGiManPutr4Pet1r", ClassName = "Pagi", - CreatedAt = new DateTime(), - ServerData = new Dictionary() { - { "username", "kevin" }, - { "sessionToken", "se551onT0k3n" } - } + CreatedAt = new DateTime { }, + ServerData = new Dictionary + { + ["username"] = "kevin", + ["sessionToken"] = "se551onT0k3n" + } }; - obj = ParseObjectExtensions.FromState(state, "Corgi"); + obj = Client.GenerateObjectFromState(state, "Corgi"); Assert.IsTrue(obj.ContainsKey("username")); Assert.IsTrue(obj.ContainsKey("sessionToken")); @@ -193,12 +204,12 @@ public void TestRemove() [TestMethod] public void TestIndexGetterSetter() { - ParseObject obj = ParseObject.Create("Corgi"); + ParseObject obj = Client.CreateObject("Corgi"); obj["gogo"] = true; obj["list"] = new List(); obj["dict"] = new Dictionary(); obj["fakeACL"] = new ParseACL(); - obj["obj"] = new ParseObject("Corgi"); + obj[nameof(obj)] = new ParseObject("Corgi"); Assert.IsTrue(obj.ContainsKey("gogo")); Assert.IsInstanceOfType(obj["gogo"], typeof(bool)); @@ -212,8 +223,8 @@ public void TestIndexGetterSetter() Assert.IsTrue(obj.ContainsKey("fakeACL")); Assert.IsInstanceOfType(obj["fakeACL"], typeof(ParseACL)); - Assert.IsTrue(obj.ContainsKey("obj")); - Assert.IsInstanceOfType(obj["obj"], typeof(ParseObject)); + Assert.IsTrue(obj.ContainsKey(nameof(obj))); + Assert.IsInstanceOfType(obj[nameof(obj)], typeof(ParseObject)); Assert.ThrowsException(() => { object gogo = obj["missingItem"]; }); } @@ -221,18 +232,21 @@ public void TestIndexGetterSetter() [TestMethod] public void TestPropertiesGetterSetter() { - DateTime now = new DateTime(); + DateTime now = new DateTime { }; + IObjectState state = new MutableObjectState { ObjectId = "waGiManPutr4Pet1r", ClassName = "Pagi", CreatedAt = now, - ServerData = new Dictionary() { - { "username", "kevin" }, - { "sessionToken", "se551onT0k3n" } - } + ServerData = new Dictionary + { + ["username"] = "kevin", + ["sessionToken"] = "se551onT0k3n" + } }; - ParseObject obj = ParseObjectExtensions.FromState(state, "Omitted"); + + ParseObject obj = Client.GenerateObjectFromState(state, "Omitted"); Assert.AreEqual("Pagi", obj.ClassName); Assert.AreEqual(now, obj.CreatedAt); @@ -246,7 +260,8 @@ public void TestPropertiesGetterSetter() [TestMethod] public void TestAddToList() { - ParseObject obj = new ParseObject("Corgi"); + ParseObject obj = new ParseObject("Corgi").Bind(Client); + obj.AddToList("emptyList", "gogo"); obj["existingList"] = new List() { "rich" }; @@ -267,7 +282,8 @@ public void TestAddToList() [TestMethod] public void TestAddUniqueToList() { - ParseObject obj = new ParseObject("Corgi"); + ParseObject obj = new ParseObject("Corgi").Bind(Client); + obj.AddUniqueToList("emptyList", "gogo"); obj["existingList"] = new List() { "gogo" }; @@ -288,7 +304,7 @@ public void TestAddUniqueToList() [TestMethod] public void TestRemoveAllFromList() { - ParseObject obj = new ParseObject("Corgi") { ["existingList"] = new List() { "gogo", "Queen of Pain" } }; + ParseObject obj = new ParseObject("Corgi", Client) { ["existingList"] = new List { "gogo", "Queen of Pain" } }; obj.RemoveAllFromList("existingList", new List() { "gogo", "missingItem" }); Assert.AreEqual(1, obj.Get>("existingList").Count); @@ -307,13 +323,16 @@ public void TestTryGetValue() { ObjectId = "waGiManPutr4Pet1r", ClassName = "Pagi", - CreatedAt = new DateTime(), - ServerData = new Dictionary() { - { "username", "kevin" }, - { "sessionToken", "se551onT0k3n" } - } + CreatedAt = new DateTime { }, + ServerData = new Dictionary() + { + ["username"] = "kevin", + ["sessionToken"] = "se551onT0k3n" + } }; - ParseObject obj = ParseObjectExtensions.FromState(state, "Omitted"); + + ParseObject obj = Client.GenerateObjectFromState(state, "Omitted"); + obj.TryGetValue("username", out string res); Assert.AreEqual("kevin", res); @@ -331,18 +350,22 @@ public void TestGet() { ObjectId = "waGiManPutr4Pet1r", ClassName = "Pagi", - CreatedAt = new DateTime(), - ServerData = new Dictionary() { - { "username", "kevin" }, - { "sessionToken", "se551onT0k3n" } - } + CreatedAt = new DateTime { }, + ServerData = new Dictionary() + { + ["username"] = "kevin", + ["sessionToken"] = "se551onT0k3n" + } }; - ParseObject obj = ParseObjectExtensions.FromState(state, "Omitted"); + + ParseObject obj = Client.GenerateObjectFromState(state, "Omitted"); Assert.AreEqual("kevin", obj.Get("username")); Assert.ThrowsException(() => obj.Get("username")); Assert.ThrowsException(() => obj.Get("missingItem")); } +#warning Some tests are not implemented. + [TestMethod] public void TestIsDataAvailable() { @@ -362,13 +385,14 @@ public void TestKeys() { ObjectId = "waGiManPutr4Pet1r", ClassName = "Pagi", - CreatedAt = new DateTime(), - ServerData = new Dictionary() { - { "username", "kevin" }, - { "sessionToken", "se551onT0k3n" } - } + CreatedAt = new DateTime { }, + ServerData = new Dictionary() + { + ["username"] = "kevin", + ["sessionToken"] = "se551onT0k3n" + } }; - ParseObject obj = ParseObjectExtensions.FromState(state, "Omitted"); + ParseObject obj = Client.GenerateObjectFromState(state, "Omitted"); Assert.AreEqual(2, obj.Keys.Count); obj["additional"] = true; @@ -385,13 +409,14 @@ public void TestAdd() { ObjectId = "waGiManPutr4Pet1r", ClassName = "Pagi", - CreatedAt = new DateTime(), - ServerData = new Dictionary() { - { "username", "kevin" }, - { "sessionToken", "se551onT0k3n" } - } + CreatedAt = new DateTime { }, + ServerData = new Dictionary() + { + ["username"] = "kevin", + ["sessionToken"] = "se551onT0k3n" + } }; - ParseObject obj = ParseObjectExtensions.FromState(state, "Omitted"); + ParseObject obj = Client.GenerateObjectFromState(state, "Omitted"); Assert.ThrowsException(() => obj.Add("username", "kevin")); obj.Add("zeus", "bewithyou"); @@ -405,43 +430,50 @@ public void TestEnumerator() { ObjectId = "waGiManPutr4Pet1r", ClassName = "Pagi", - CreatedAt = new DateTime(), - ServerData = new Dictionary() { - { "username", "kevin" }, - { "sessionToken", "se551onT0k3n" } - } + CreatedAt = new DateTime { }, + ServerData = new Dictionary() + { + ["username"] = "kevin", + ["sessionToken"] = "se551onT0k3n" + } }; - ParseObject obj = ParseObjectExtensions.FromState(state, "Omitted"); + ParseObject obj = Client.GenerateObjectFromState(state, "Omitted"); int count = 0; + foreach (KeyValuePair key in obj) { count++; } + Assert.AreEqual(2, count); obj["newDirtyItem"] = "newItem"; count = 0; + foreach (KeyValuePair key in obj) { count++; } + Assert.AreEqual(3, count); } [TestMethod] public void TestGetQuery() { - ParseObject.RegisterSubclass(); + Client.AddValidClass(); - ParseQuery query = ParseObject.GetQuery("UnregisteredSubClass"); - Assert.AreEqual("UnregisteredSubClass", query.GetClassName()); + ParseQuery query = Client.GetQuery(nameof(UnregisteredSubClass)); + Assert.AreEqual(nameof(UnregisteredSubClass), query.GetClassName()); - Assert.ThrowsException(() => ParseObject.GetQuery("SubClass")); + Assert.ThrowsException(() => Client.GetQuery(nameof(SubClass))); - ParseCorePlugins.Instance.SubclassingController.UnregisterSubclass(typeof(SubClass)); + Client.ClassController.RemoveClass(typeof(SubClass)); } +#warning These tests are incomplete. + [TestMethod] public void TestPropertyChanged() { @@ -450,50 +482,38 @@ public void TestPropertyChanged() [TestMethod] [AsyncStateMachine(typeof(ObjectTests))] - public Task TestSave() - { + public Task TestSave() => // TODO (hallucinogen): do this - return Task.FromResult(0); - } + Task.FromResult(0); [TestMethod] [AsyncStateMachine(typeof(ObjectTests))] - public Task TestSaveAll() - { + public Task TestSaveAll() => // TODO (hallucinogen): do this - return Task.FromResult(0); - } + Task.FromResult(0); [TestMethod] [AsyncStateMachine(typeof(ObjectTests))] - public Task TestDelete() - { + public Task TestDelete() => // TODO (hallucinogen): do this - return Task.FromResult(0); - } + Task.FromResult(0); [TestMethod] [AsyncStateMachine(typeof(ObjectTests))] - public Task TestDeleteAll() - { + public Task TestDeleteAll() => // TODO (hallucinogen): do this - return Task.FromResult(0); - } + Task.FromResult(0); [TestMethod] [AsyncStateMachine(typeof(ObjectTests))] - public Task TestFetch() - { + public Task TestFetch() => // TODO (hallucinogen): do this - return Task.FromResult(0); - } + Task.FromResult(0); [TestMethod] [AsyncStateMachine(typeof(ObjectTests))] - public Task TestFetchAll() - { + public Task TestFetchAll() => // TODO (hallucinogen): do this - return Task.FromResult(0); - } + Task.FromResult(0); } } diff --git a/Parse.Test/Parse.Test.csproj b/Parse.Tests/Parse.Tests.csproj similarity index 92% rename from Parse.Test/Parse.Test.csproj rename to Parse.Tests/Parse.Tests.csproj index b319c947..5ebd6543 100644 --- a/Parse.Test/Parse.Test.csproj +++ b/Parse.Tests/Parse.Tests.csproj @@ -1,7 +1,7 @@ - netcoreapp2.1 + netcoreapp3.1 false latest diff --git a/Parse.Tests/ProgressTests.cs b/Parse.Tests/ProgressTests.cs new file mode 100644 index 00000000..5268f646 --- /dev/null +++ b/Parse.Tests/ProgressTests.cs @@ -0,0 +1,68 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Parse.Abstractions.Infrastructure; +using Parse.Infrastructure; + +namespace Parse.Tests +{ +#warning Refactor if possible. + + [TestClass] + public class ProgressTests + { + [TestMethod] + public void TestDownloadProgressEventGetterSetter() + { + IDataTransferLevel downloadProgressEvent = new DataTransferLevel { Amount = 0.5f }; + Assert.AreEqual(0.5f, downloadProgressEvent.Amount); + + downloadProgressEvent.Amount = 1.0f; + Assert.AreEqual(1.0f, downloadProgressEvent.Amount); + } + + [TestMethod] + public void TestUploadProgressEventGetterSetter() + { + IDataTransferLevel uploadProgressEvent = new DataTransferLevel { Amount = 0.5f }; + Assert.AreEqual(0.5f, uploadProgressEvent.Amount); + + uploadProgressEvent.Amount = 1.0f; + Assert.AreEqual(1.0f, uploadProgressEvent.Amount); + } + + [TestMethod] + public void TestObservingDownloadProgress() + { + int called = 0; + Mock> mockProgress = new Mock>(); + mockProgress.Setup(obj => obj.Report(It.IsAny())).Callback(() => called++); + IProgress progress = mockProgress.Object; + + progress.Report(new DataTransferLevel { Amount = 0.2f }); + progress.Report(new DataTransferLevel { Amount = 0.42f }); + progress.Report(new DataTransferLevel { Amount = 0.53f }); + progress.Report(new DataTransferLevel { Amount = 0.68f }); + progress.Report(new DataTransferLevel { Amount = 0.88f }); + + Assert.AreEqual(5, called); + } + + [TestMethod] + public void TestObservingUploadProgress() + { + int called = 0; + Mock> mockProgress = new Mock>(); + mockProgress.Setup(obj => obj.Report(It.IsAny())).Callback(() => called++); + IProgress progress = mockProgress.Object; + + progress.Report(new DataTransferLevel { Amount = 0.2f }); + progress.Report(new DataTransferLevel { Amount = 0.42f }); + progress.Report(new DataTransferLevel { Amount = 0.53f }); + progress.Report(new DataTransferLevel { Amount = 0.68f }); + progress.Report(new DataTransferLevel { Amount = 0.88f }); + + Assert.AreEqual(5, called); + } + } +} diff --git a/Parse.Test/PushEncoderTests.cs b/Parse.Tests/PushEncoderTests.cs similarity index 57% rename from Parse.Test/PushEncoderTests.cs rename to Parse.Tests/PushEncoderTests.cs index b8268099..0a2d583f 100644 --- a/Parse.Test/PushEncoderTests.cs +++ b/Parse.Tests/PushEncoderTests.cs @@ -1,12 +1,10 @@ -using Parse.Push.Internal; using System; -using System.Linq; using System.Collections.Generic; - using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; +using Parse.Platform.Push; -namespace Parse.Test +namespace Parse.Tests { [TestClass] public class PushEncoderTests @@ -20,7 +18,7 @@ public void TestEncodeEmpty() state.Alert = "alert"; Assert.ThrowsException(() => ParsePushEncoder.Instance.Encode(state)); - state.Channels = new List { { "channel" } }; + state.Channels = new List { "channel" }; ParsePushEncoder.Instance.Encode(state); } @@ -30,28 +28,27 @@ public void TestEncode() { MutablePushState state = new MutablePushState { - Data = new Dictionary { - { "alert", "Some Alert" } - }, - Channels = new List { - { "channel" } - } + Data = new Dictionary + { + ["alert"] = "Some Alert" + }, + Channels = new List { "channel" } }; - IDictionary expected = new Dictionary { - { - "data", new Dictionary {{ - "alert", "Some Alert" - }} - }, - { - "where", new Dictionary {{ - "channels", new Dictionary {{ - "$in", new List {{ "channel" }} - }} - }} - } - }; + IDictionary expected = new Dictionary + { + ["data"] = new Dictionary + { + ["alert"] = "Some Alert" + }, + ["where"] = new Dictionary + { + ["channels"] = new Dictionary + { + ["$in"] = new List { "channel" } + } + } + }; Assert.AreEqual(JsonConvert.SerializeObject(expected), JsonConvert.SerializeObject(ParsePushEncoder.Instance.Encode(state))); } diff --git a/Parse.Test/PushStateTests.cs b/Parse.Tests/PushStateTests.cs similarity index 82% rename from Parse.Test/PushStateTests.cs rename to Parse.Tests/PushStateTests.cs index 041d9ca3..bc08fb8a 100644 --- a/Parse.Test/PushStateTests.cs +++ b/Parse.Tests/PushStateTests.cs @@ -1,8 +1,8 @@ -using Parse.Push.Internal; - using Microsoft.VisualStudio.TestTools.UnitTesting; +using Parse.Abstractions.Platform.Push; +using Parse.Platform.Push; -namespace Parse.Test +namespace Parse.Tests { [TestClass] public class PushStateTests @@ -12,10 +12,7 @@ public void TestMutatedClone() { MutablePushState state = new MutablePushState(); - IPushState mutated = state.MutatedClone(s => - { - s.Alert = "test"; - }); + IPushState mutated = state.MutatedClone(s => s.Alert = "test"); Assert.AreEqual(null, state.Alert); Assert.AreEqual("test", mutated.Alert); diff --git a/Parse.Tests/PushTests.cs b/Parse.Tests/PushTests.cs new file mode 100644 index 00000000..6b5f609b --- /dev/null +++ b/Parse.Tests/PushTests.cs @@ -0,0 +1,152 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Platform.Push; +using Parse.Infrastructure.Utilities; +using Parse.Infrastructure; +using Parse.Platform.Push; + +namespace Parse.Tests +{ + [TestClass] + public class PushTests + { + ParseClient Client { get; } = new ParseClient(new ServerConnectionData { Test = true }); + + IParsePushController GetMockedPushController(IPushState expectedPushState) + { + Mock mockedController = new Mock(MockBehavior.Strict); + mockedController.Setup(obj => obj.SendPushNotificationAsync(It.Is(s => s.Equals(expectedPushState)), It.IsAny(), It.IsAny())).Returns(Task.FromResult(false)); + + return mockedController.Object; + } + + IParsePushChannelsController GetMockedPushChannelsController(IEnumerable channels) + { + Mock mockedChannelsController = new Mock(MockBehavior.Strict); + mockedChannelsController.Setup(obj => obj.SubscribeAsync(It.Is>(it => it.CollectionsEqual(channels)), It.IsAny(), It.IsAny())).Returns(Task.FromResult(false)); + mockedChannelsController.Setup(obj => obj.UnsubscribeAsync(It.Is>(it => it.CollectionsEqual(channels)), It.IsAny(), It.IsAny())).Returns(Task.FromResult(false)); + + return mockedChannelsController.Object; + } + + [TestCleanup] + public void TearDown() => (Client.Services as ServiceHub).Reset(); + + [TestMethod] + [AsyncStateMachine(typeof(PushTests))] + public Task TestSendPush() + { + MutableServiceHub hub = new MutableServiceHub { }; + ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + MutablePushState state = new MutablePushState + { + Query = Client.GetInstallationQuery() + }; + + ParsePush thePush = new ParsePush(client); + + hub.PushController = GetMockedPushController(state); + + thePush.Alert = "Alert"; + state.Alert = "Alert"; + + return thePush.SendAsync().ContinueWith(task => + { + Assert.IsTrue(task.IsCompleted); + Assert.IsFalse(task.IsFaulted); + + thePush.Channels = new List { { "channel" } }; + state.Channels = new List { { "channel" } }; + + return thePush.SendAsync(); + }).Unwrap().ContinueWith(task => + { + Assert.IsTrue(task.IsCompleted); + Assert.IsFalse(task.IsFaulted); + + ParseQuery query = new ParseQuery(client, "aClass"); + + thePush.Query = query; + state.Query = query; + + return thePush.SendAsync(); + }).Unwrap().ContinueWith(task => + { + Assert.IsTrue(task.IsCompleted); + Assert.IsFalse(task.IsFaulted); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(PushTests))] + public Task TestSubscribe() + { + MutableServiceHub hub = new MutableServiceHub { }; + ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + List channels = new List { }; + + hub.PushChannelsController = GetMockedPushChannelsController(channels); + + channels.Add("test"); + + return client.SubscribeToPushChannelAsync("test").ContinueWith(task => + { + Assert.IsTrue(task.IsCompleted); + Assert.IsFalse(task.IsFaulted); + + return client.SubscribeToPushChannelsAsync(new List { "test" }); + }).Unwrap().ContinueWith(task => + { + Assert.IsTrue(task.IsCompleted); + Assert.IsFalse(task.IsFaulted); + + CancellationTokenSource cancellationTokenSource = new CancellationTokenSource { }; + return client.SubscribeToPushChannelsAsync(new List { "test" }, cancellationTokenSource.Token); + }).Unwrap().ContinueWith(task => + { + Assert.IsTrue(task.IsCompleted); + Assert.IsFalse(task.IsFaulted); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(PushTests))] + public Task TestUnsubscribe() + { + MutableServiceHub hub = new MutableServiceHub { }; + ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + List channels = new List { }; + + hub.PushChannelsController = GetMockedPushChannelsController(channels); + + channels.Add("test"); + + return client.UnsubscribeToPushChannelAsync("test").ContinueWith(task => + { + Assert.IsTrue(task.IsCompleted); + Assert.IsFalse(task.IsFaulted); + + return client.UnsubscribeToPushChannelsAsync(new List { { "test" } }); + }).ContinueWith(task => + { + Assert.IsTrue(task.IsCompleted); + Assert.IsFalse(task.IsFaulted); + + CancellationTokenSource cancellationTokenSource = new CancellationTokenSource { }; + return client.UnsubscribeToPushChannelsAsync(new List { { "test" } }, cancellationTokenSource.Token); + }).ContinueWith(task => + { + Assert.IsTrue(task.IsCompleted); + Assert.IsFalse(task.IsFaulted); + }); + } + } +} diff --git a/Parse.Test/RelationTests.cs b/Parse.Tests/RelationTests.cs similarity index 81% rename from Parse.Test/RelationTests.cs rename to Parse.Tests/RelationTests.cs index 9f4269d6..d0929b21 100644 --- a/Parse.Test/RelationTests.cs +++ b/Parse.Tests/RelationTests.cs @@ -1,10 +1,9 @@ -using Parse; -using Parse.Core.Internal; using System.Collections.Generic; - using Microsoft.VisualStudio.TestTools.UnitTesting; +using Parse.Abstractions.Internal; +using Parse.Infrastructure; -namespace Parse.Test +namespace Parse.Tests { [TestClass] public class RelationTests @@ -12,7 +11,7 @@ public class RelationTests [TestMethod] public void TestRelationQuery() { - ParseObject parent = ParseObject.CreateWithoutData("Foo", "abcxyz"); + ParseObject parent = new ServiceHub { }.CreateObjectWithoutData("Foo", "abcxyz"); ParseRelation relation = parent.GetRelation("child"); ParseQuery query = relation.Query; diff --git a/Parse.Tests/SessionControllerTests.cs b/Parse.Tests/SessionControllerTests.cs new file mode 100644 index 00000000..454ae0ac --- /dev/null +++ b/Parse.Tests/SessionControllerTests.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Execution; +using Parse.Abstractions.Platform.Objects; +using Parse.Abstractions.Platform.Sessions; +using Parse.Infrastructure; +using Parse.Infrastructure.Execution; +using Parse.Platform.Sessions; + +namespace Parse.Tests +{ + [TestClass] + public class SessionControllerTests + { +#warning Check if reinitializing the client for every test method is really necessary. + + ParseClient Client { get; set; } + + [TestInitialize] + public void SetUp() => Client = new ParseClient(new ServerConnectionData { ApplicationID = "", Key = "", Test = true }); + + [TestMethod] + [AsyncStateMachine(typeof(SessionControllerTests))] + public Task TestGetSessionWithEmptyResult() => new ParseSessionController(CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, null)).Object, Client.Decoder).GetSessionAsync("S0m3Se551on", Client, CancellationToken.None).ContinueWith(task => + { + Assert.IsTrue(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + }); + + [TestMethod] + [AsyncStateMachine(typeof(SessionControllerTests))] + public Task TestGetSession() + { + Tuple> response = new Tuple>(HttpStatusCode.Accepted, new Dictionary + { + ["__type"] = "Object", + ["className"] = "Session", + ["sessionToken"] = "S0m3Se551on", + ["restricted"] = true + }); + + Mock mockRunner = CreateMockRunner(response); + + return new ParseSessionController(mockRunner.Object, Client.Decoder).GetSessionAsync("S0m3Se551on", Client, CancellationToken.None).ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + + mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "sessions/me"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); + + IObjectState session = task.Result; + Assert.AreEqual(2, session.Count()); + Assert.IsTrue((bool) session["restricted"]); + Assert.AreEqual("S0m3Se551on", session["sessionToken"]); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(SessionControllerTests))] + public Task TestRevoke() + { + Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, default)); + + return new ParseSessionController(mockRunner.Object, Client.Decoder).RevokeAsync("S0m3Se551on", CancellationToken.None).ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + + mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "logout"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(SessionControllerTests))] + public Task TestUpgradeToRevocableSession() + { + Tuple> response = new Tuple>(HttpStatusCode.Accepted, + new Dictionary + { + ["__type"] = "Object", + ["className"] = "Session", + ["sessionToken"] = "S0m3Se551on", + ["restricted"] = true + }); + + Mock mockRunner = CreateMockRunner(response); + + return new ParseSessionController(mockRunner.Object, Client.Decoder).UpgradeToRevocableSessionAsync("S0m3Se551on", Client, CancellationToken.None).ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "upgradeToRevocableSession"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); + + IObjectState session = task.Result; + Assert.AreEqual(2, session.Count()); + Assert.IsTrue((bool) session["restricted"]); + Assert.AreEqual("S0m3Se551on", session["sessionToken"]); + }); + } + + [TestMethod] + public void TestIsRevocableSessionToken() + { + IParseSessionController sessionController = new ParseSessionController(Mock.Of(), Client.Decoder); + Assert.IsTrue(sessionController.IsRevocableSessionToken("r:session")); + Assert.IsTrue(sessionController.IsRevocableSessionToken("r:session:r:")); + Assert.IsTrue(sessionController.IsRevocableSessionToken("session:r:")); + Assert.IsFalse(sessionController.IsRevocableSessionToken("session:s:d:r")); + Assert.IsFalse(sessionController.IsRevocableSessionToken("s:ession:s:d:r")); + Assert.IsFalse(sessionController.IsRevocableSessionToken("")); + } + + + private Mock CreateMockRunner(Tuple> response) + { + Mock mockRunner = new Mock(); + mockRunner.Setup(obj => obj.RunCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(response)); + + return mockRunner; + } + } +} diff --git a/Parse.Tests/SessionTests.cs b/Parse.Tests/SessionTests.cs new file mode 100644 index 00000000..2bff624e --- /dev/null +++ b/Parse.Tests/SessionTests.cs @@ -0,0 +1,167 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Platform.Objects; +using Parse.Abstractions.Platform.Sessions; +using Parse.Abstractions.Platform.Users; +using Parse.Infrastructure; +using Parse.Platform.Objects; + +namespace Parse.Tests +{ + [TestClass] + public class SessionTests + { + ParseClient Client { get; } = new ParseClient(new ServerConnectionData { Test = true }); + + [TestInitialize] + public void SetUp() + { + Client.AddValidClass(); + Client.AddValidClass(); + } + + [TestCleanup] + public void TearDown() => (Client.Services as ServiceHub).Reset(); + + [TestMethod] + public void TestGetSessionQuery() => Assert.IsInstanceOfType(Client.GetSessionQuery(), typeof(ParseQuery)); + + [TestMethod] + public void TestGetSessionToken() + { + ParseSession session = Client.GenerateObjectFromState(new MutableObjectState { ServerData = new Dictionary() { ["sessionToken"] = "llaKcolnu" } }, "_Session"); + + Assert.IsNotNull(session); + Assert.AreEqual("llaKcolnu", session.SessionToken); + } + + [TestMethod] + [AsyncStateMachine(typeof(SessionTests))] + public Task TestGetCurrentSession() + { + MutableServiceHub hub = new MutableServiceHub { }; + ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + IObjectState sessionState = new MutableObjectState + { + ServerData = new Dictionary + { + ["sessionToken"] = "newllaKcolnu" + } + }; + + Mock mockController = new Mock(); + mockController.Setup(obj => obj.GetSessionAsync(It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(sessionState)); + + IObjectState userState = new MutableObjectState + { + ServerData = new Dictionary + { + ["sessionToken"] = "llaKcolnu" + } + }; + + ParseUser user = client.GenerateObjectFromState(userState, "_User"); + + Mock mockCurrentUserController = new Mock(); + mockCurrentUserController.Setup(obj => obj.GetAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(user)); + + hub.SessionController = mockController.Object; + hub.CurrentUserController = mockCurrentUserController.Object; + + return client.GetCurrentSessionAsync().ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + + mockController.Verify(obj => obj.GetSessionAsync(It.Is(sessionToken => sessionToken == "llaKcolnu"), It.IsAny(),It.IsAny()), Times.Exactly(1)); + + ParseSession session = task.Result; + Assert.AreEqual("newllaKcolnu", session.SessionToken); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(SessionTests))] + public Task TestGetCurrentSessionWithNoCurrentUser() + { + MutableServiceHub hub = new MutableServiceHub { }; + ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + Mock mockController = new Mock(); + Mock mockCurrentUserController = new Mock(); + + hub.SessionController = mockController.Object; + hub.CurrentUserController = mockCurrentUserController.Object; + + return client.GetCurrentSessionAsync().ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + Assert.IsNull(task.Result); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(SessionTests))] + public Task TestRevoke() + { + MutableServiceHub hub = new MutableServiceHub { }; + ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + Mock mockController = new Mock(); + mockController.Setup(sessionController => sessionController.IsRevocableSessionToken(It.IsAny())).Returns(true); + + hub.SessionController = mockController.Object; + + CancellationTokenSource source = new CancellationTokenSource { }; + return client.RevokeSessionAsync("r:someSession", source.Token).ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + + mockController.Verify(obj => obj.RevokeAsync(It.Is(sessionToken => sessionToken == "r:someSession"), source.Token), Times.Exactly(1)); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(SessionTests))] + public Task TestUpgradeToRevocableSession() + { + MutableServiceHub hub = new MutableServiceHub { }; + ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + IObjectState state = new MutableObjectState + { + ServerData = new Dictionary() + { + ["sessionToken"] = "llaKcolnu" + } + }; + + Mock mockController = new Mock(); + mockController.Setup(obj => obj.UpgradeToRevocableSessionAsync(It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(state)); + + Mock mockCurrentUserController = new Mock(); + + hub.SessionController = mockController.Object; + hub.CurrentUserController = mockCurrentUserController.Object; + + CancellationTokenSource source = new CancellationTokenSource { }; + return client.UpgradeToRevocableSessionAsync("someSession", source.Token).ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + + mockController.Verify(obj => obj.UpgradeToRevocableSessionAsync(It.Is(sessionToken => sessionToken == "someSession"), It.IsAny(), source.Token), Times.Exactly(1)); + + Assert.AreEqual("llaKcolnu", task.Result); + }); + } + } +} diff --git a/Parse.Tests/UserControllerTests.cs b/Parse.Tests/UserControllerTests.cs new file mode 100644 index 00000000..edb2ac12 --- /dev/null +++ b/Parse.Tests/UserControllerTests.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Control; +using Parse.Abstractions.Infrastructure.Execution; +using Parse.Abstractions.Platform.Objects; +using Parse.Infrastructure; +using Parse.Infrastructure.Execution; +using Parse.Platform.Objects; +using Parse.Platform.Users; + +namespace Parse.Tests +{ + [TestClass] + public class UserControllerTests + { + ParseClient Client { get; set; } + + [TestInitialize] + public void SetUp() => Client = new ParseClient(new ServerConnectionData { ApplicationID = "", Key = "", Test = true }); + + [TestMethod] + [AsyncStateMachine(typeof(UserControllerTests))] + public Task TestSignUp() + { + MutableObjectState state = new MutableObjectState + { + ClassName = "_User", + ServerData = new Dictionary + { + ["username"] = "hallucinogen", + ["password"] = "secret" + } + }; + + Dictionary operations = new Dictionary + { + ["gogo"] = new Mock().Object + }; + + Dictionary responseDict = new Dictionary + { + ["__type"] = "Object", + ["className"] = "_User", + ["objectId"] = "d3ImSh3ki", + ["sessionToken"] = "s3ss10nt0k3n", + ["createdAt"] = "2015-09-18T18:11:28.943Z" + }; + + Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, responseDict)); + + return new ParseUserController(mockRunner.Object, Client.Decoder).SignUpAsync(state, operations, Client, CancellationToken.None).ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + + mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "classes/_User"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); + + IObjectState newState = task.Result; + Assert.AreEqual("s3ss10nt0k3n", newState["sessionToken"]); + Assert.AreEqual("d3ImSh3ki", newState.ObjectId); + Assert.IsNotNull(newState.CreatedAt); + Assert.IsNotNull(newState.UpdatedAt); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(UserControllerTests))] + public Task TestLogInWithUsernamePassword() + { + Dictionary responseDict = new Dictionary + { + ["__type"] = "Object", + ["className"] = "_User", + ["objectId"] = "d3ImSh3ki", + ["sessionToken"] = "s3ss10nt0k3n", + ["createdAt"] = "2015-09-18T18:11:28.943Z" + }; + + Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, responseDict)); + + return new ParseUserController(mockRunner.Object, Client.Decoder).LogInAsync("grantland", "123grantland123", Client, CancellationToken.None).ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + + mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "login?username=grantland&password=123grantland123"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); + + IObjectState newState = task.Result; + Assert.AreEqual("s3ss10nt0k3n", newState["sessionToken"]); + Assert.AreEqual("d3ImSh3ki", newState.ObjectId); + Assert.IsNotNull(newState.CreatedAt); + Assert.IsNotNull(newState.UpdatedAt); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(UserControllerTests))] + public Task TestLogInWithAuthData() + { + Dictionary responseDict = new Dictionary + { + ["__type"] = "Object" , + ["className"] = "_User" , + ["objectId"] = "d3ImSh3ki" , + ["sessionToken"] = "s3ss10nt0k3n" , + ["createdAt"] = "2015-09-18T18:11:28.943Z" + }; + + Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, responseDict)); + + return new ParseUserController(mockRunner.Object, Client.Decoder).LogInAsync("facebook", data: null, serviceHub: Client, cancellationToken: CancellationToken.None).ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + + mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "users"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); + + IObjectState newState = task.Result; + Assert.AreEqual("s3ss10nt0k3n", newState["sessionToken"]); + Assert.AreEqual("d3ImSh3ki", newState.ObjectId); + Assert.IsNotNull(newState.CreatedAt); + Assert.IsNotNull(newState.UpdatedAt); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(UserControllerTests))] + public Task TestGetUserFromSessionToken() + { + Dictionary responseDict = new Dictionary + { + ["__type"] = "Object", + ["className"] = "_User", + ["objectId"] = "d3ImSh3ki", + ["sessionToken"] = "s3ss10nt0k3n", + ["createdAt"] = "2015-09-18T18:11:28.943Z" + }; + + Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, responseDict)); + + return new ParseUserController(mockRunner.Object, Client.Decoder).GetUserAsync("s3ss10nt0k3n", Client, CancellationToken.None).ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + + mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "users/me"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); + + IObjectState newState = task.Result; + Assert.AreEqual("s3ss10nt0k3n", newState["sessionToken"]); + Assert.AreEqual("d3ImSh3ki", newState.ObjectId); + Assert.IsNotNull(newState.CreatedAt); + Assert.IsNotNull(newState.UpdatedAt); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(UserControllerTests))] + public Task TestRequestPasswordReset() + { + Mock mockRunner = CreateMockRunner(new Tuple>(HttpStatusCode.Accepted, new Dictionary { })); + + return new ParseUserController(mockRunner.Object, Client.Decoder).RequestPasswordResetAsync("gogo@parse.com", CancellationToken.None).ContinueWith(t => + { + Assert.IsFalse(t.IsFaulted); + Assert.IsFalse(t.IsCanceled); + + mockRunner.Verify(obj => obj.RunCommandAsync(It.Is(command => command.Path == "requestPasswordReset"), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Exactly(1)); + }); + } + + Mock CreateMockRunner(Tuple> response) + { + Mock mockRunner = new Mock { }; + mockRunner.Setup(obj => obj.RunCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny())).Returns(Task.FromResult(response)); + + return mockRunner; + } + } +} diff --git a/Parse.Tests/UserTests.cs b/Parse.Tests/UserTests.cs new file mode 100644 index 00000000..4e205ff2 --- /dev/null +++ b/Parse.Tests/UserTests.cs @@ -0,0 +1,790 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Parse.Abstractions.Infrastructure; +using Parse.Infrastructure; +using Parse.Abstractions.Infrastructure.Control; +using Parse.Abstractions.Platform.Objects; +using Parse.Abstractions.Platform.Sessions; +using Parse.Abstractions.Platform.Users; +using Parse.Platform.Objects; + +namespace Parse.Tests +{ +#warning Class refactoring requires completion. + + [TestClass] + public class UserTests + { + ParseClient Client { get; set; } = new ParseClient(new ServerConnectionData { Test = true }); + + [TestCleanup] + public void TearDown() => (Client.Services as ServiceHub).Reset(); + + [TestMethod] + public void TestRemoveFields() + { + IObjectState state = new MutableObjectState + { + ServerData = new Dictionary + { + ["username"] = "kevin", + ["name"] = "andrew" + } + }; + + ParseUser user = Client.GenerateObjectFromState(state, "_User"); + Assert.ThrowsException(() => user.Remove("username")); + + try + { + user.Remove("name"); + } + catch + { + Assert.Fail(@"Removing ""name"" field on ParseUser should not throw an exception because ""name"" is not an immutable field and was defined on the object."); + } + + Assert.IsFalse(user.ContainsKey("name")); + } + + [TestMethod] + public void TestSessionTokenGetter() + { + IObjectState state = new MutableObjectState + { + ServerData = new Dictionary + { + ["username"] = "kevin", + ["sessionToken"] = "se551onT0k3n" + } + }; + + ParseUser user = Client.GenerateObjectFromState(state, "_User"); + Assert.AreEqual("se551onT0k3n", user.SessionToken); + } + + [TestMethod] + public void TestUsernameGetterSetter() + { + IObjectState state = new MutableObjectState + { + ServerData = new Dictionary + { + ["username"] = "kevin", + } + }; + + ParseUser user = Client.GenerateObjectFromState(state, "_User"); + Assert.AreEqual("kevin", user.Username); + user.Username = "ilya"; + Assert.AreEqual("ilya", user.Username); + } + + [TestMethod] + public void TestPasswordGetterSetter() + { + IObjectState state = new MutableObjectState + { + ServerData = new Dictionary + { + ["username"] = "kevin", + ["password"] = "hurrah" + } + }; + + ParseUser user = Client.GenerateObjectFromState(state, "_User"); + Assert.AreEqual("hurrah", user.State["password"]); + user.Password = "david"; + Assert.IsNotNull(user.CurrentOperations["password"]); + } + + [TestMethod] + public void TestEmailGetterSetter() + { + IObjectState state = new MutableObjectState + { + ServerData = new Dictionary + { + ["email"] = "james@parse.com", + ["name"] = "andrew", + ["sessionToken"] = "se551onT0k3n" + } + }; + + ParseUser user = Client.GenerateObjectFromState(state, "_User"); + Assert.AreEqual("james@parse.com", user.Email); + user.Email = "bryan@parse.com"; + Assert.AreEqual("bryan@parse.com", user.Email); + } + + [TestMethod] + public void TestAuthDataGetter() + { + IObjectState state = new MutableObjectState + { + ServerData = new Dictionary + { + ["email"] = "james@parse.com", + ["authData"] = new Dictionary + { + ["facebook"] = new Dictionary + { + ["sessionToken"] = "none" + } + } + } + }; + + ParseUser user = Client.GenerateObjectFromState(state, "_User"); + Assert.AreEqual(1, user.AuthData.Count); + Assert.IsInstanceOfType(user.AuthData["facebook"], typeof(IDictionary)); + } + + [TestMethod] + public void TestGetUserQuery() => Assert.IsInstanceOfType(Client.GetUserQuery(), typeof(ParseQuery)); + + [TestMethod] + public void TestIsAuthenticated() + { + IObjectState state = new MutableObjectState + { + ObjectId = "wagimanPutraPetir", + ServerData = new Dictionary + { + ["sessionToken"] = "llaKcolnu" + } + }; + + MutableServiceHub hub = new MutableServiceHub { }; + ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + ParseUser user = client.GenerateObjectFromState(state, "_User"); + + Mock mockCurrentUserController = new Mock { }; + mockCurrentUserController.Setup(obj => obj.GetAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(user)); + + hub.CurrentUserController = mockCurrentUserController.Object; + + Assert.IsTrue(user.IsAuthenticated); + } + + [TestMethod] + public void TestIsAuthenticatedWithOtherParseUser() + { + IObjectState state = new MutableObjectState + { + ObjectId = "wagimanPutraPetir", + ServerData = new Dictionary + { + ["sessionToken"] = "llaKcolnu" + } + }; + + IObjectState state2 = new MutableObjectState + { + ObjectId = "wagimanPutraPetir2", + ServerData = new Dictionary + { + ["sessionToken"] = "llaKcolnu" + } + }; + + MutableServiceHub hub = new MutableServiceHub { }; + ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + ParseUser user = client.GenerateObjectFromState(state, "_User"); + ParseUser user2 = client.GenerateObjectFromState(state2, "_User"); + + Mock mockCurrentUserController = new Mock { }; + mockCurrentUserController.Setup(obj => obj.GetAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(user)); + + hub.CurrentUserController = mockCurrentUserController.Object; + + Assert.IsFalse(user2.IsAuthenticated); + } + + [TestMethod] + [AsyncStateMachine(typeof(UserTests))] + public Task TestSignUpWithInvalidServerData() + { + IObjectState state = new MutableObjectState + { + ServerData = new Dictionary + { + ["sessionToken"] = "llaKcolnu" + } + }; + + ParseUser user = Client.GenerateObjectFromState(state, "_User"); + + return user.SignUpAsync().ContinueWith(task => + { + Assert.IsTrue(task.IsFaulted); + Assert.IsInstanceOfType(task.Exception.InnerException, typeof(InvalidOperationException)); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(UserTests))] + public Task TestSignUp() + { + IObjectState state = new MutableObjectState + { + ServerData = new Dictionary + { + ["sessionToken"] = "llaKcolnu", + ["username"] = "ihave", + ["password"] = "adream" + } + }; + + IObjectState newState = new MutableObjectState + { + ObjectId = "some0neTol4v4" + }; + + MutableServiceHub hub = new MutableServiceHub { }; + ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + ParseUser user = client.GenerateObjectFromState(state, "_User"); + + Mock mockController = new Mock { }; + mockController.Setup(obj => obj.SignUpAsync(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(newState)); + + hub.UserController = mockController.Object; + + return user.SignUpAsync().ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + + mockController.Verify(obj => obj.SignUpAsync(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny()), Times.Exactly(1)); + + Assert.IsFalse(user.IsDirty); + Assert.AreEqual("ihave", user.Username); + Assert.IsFalse(user.State.ContainsKey("password")); + Assert.AreEqual("some0neTol4v4", user.ObjectId); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(UserTests))] + public Task TestLogIn() + { + IObjectState state = new MutableObjectState + { + ServerData = new Dictionary + { + ["sessionToken"] = "llaKcolnu", + ["username"] = "ihave", + ["password"] = "adream" + } + }; + + IObjectState newState = new MutableObjectState + { + ObjectId = "some0neTol4v4" + }; + + MutableServiceHub hub = new MutableServiceHub { }; + ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + Mock mockController = new Mock { }; + mockController.Setup(obj => obj.LogInAsync("ihave", "adream", It.IsAny(), It.IsAny())).Returns(Task.FromResult(newState)); + + hub.UserController = mockController.Object; + + return client.LogInAsync("ihave", "adream").ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + + mockController.Verify(obj => obj.LogInAsync("ihave", "adream", It.IsAny(), It.IsAny()), Times.Exactly(1)); + + ParseUser user = task.Result; + Assert.IsFalse(user.IsDirty); + Assert.IsNull(user.Username); + Assert.AreEqual("some0neTol4v4", user.ObjectId); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(UserTests))] + public Task TestBecome() + { + IObjectState state = new MutableObjectState + { + ObjectId = "some0neTol4v4", + ServerData = new Dictionary { ["sessionToken"] = "llaKcolnu" } + }; + + MutableServiceHub hub = new MutableServiceHub { }; + ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + Mock mockController = new Mock { }; + mockController.Setup(obj => obj.GetUserAsync("llaKcolnu", It.IsAny(), It.IsAny())).Returns(Task.FromResult(state)); + + hub.UserController = mockController.Object; + + return client.BecomeAsync("llaKcolnu").ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + + mockController.Verify(obj => obj.GetUserAsync("llaKcolnu", It.IsAny(), It.IsAny()), Times.Exactly(1)); + + ParseUser user = task.Result; + Assert.AreEqual("some0neTol4v4", user.ObjectId); + Assert.AreEqual("llaKcolnu", user.SessionToken); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(UserTests))] + public Task TestLogOut() + { + IObjectState state = new MutableObjectState + { + ServerData = new Dictionary + { + ["sessionToken"] = "r:llaKcolnu" + } + }; + + MutableServiceHub hub = new MutableServiceHub { }; + ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + ParseUser user = client.GenerateObjectFromState(state, "_User"); + + Mock mockCurrentUserController = new Mock { }; + mockCurrentUserController.Setup(obj => obj.GetAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(user)); + + Mock mockSessionController = new Mock(); + mockSessionController.Setup(c => c.IsRevocableSessionToken(It.IsAny())).Returns(true); + + hub.CurrentUserController = mockCurrentUserController.Object; + hub.SessionController = mockSessionController.Object; + + return client.LogOutAsync().ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + + mockCurrentUserController.Verify(obj => obj.LogOutAsync(It.IsAny(), It.IsAny()), Times.Exactly(1)); + mockSessionController.Verify(obj => obj.RevokeAsync("r:llaKcolnu", It.IsAny()), Times.Exactly(1)); + }); + } + + [TestMethod] + public void TestCurrentUser() + { + IObjectState state = new MutableObjectState + { + ServerData = new Dictionary + { + ["sessionToken"] = "llaKcolnu" + } + }; + + MutableServiceHub hub = new MutableServiceHub { }; + ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + ParseUser user = client.GenerateObjectFromState(state, "_User"); + + Mock mockCurrentUserController = new Mock { }; + mockCurrentUserController.Setup(obj => obj.GetAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(user)); + + hub.CurrentUserController = mockCurrentUserController.Object; + + Assert.AreEqual(user, client.GetCurrentUser()); + } + + [TestMethod] + public void TestCurrentUserWithEmptyResult() => Assert.IsNull(new ParseClient(new ServerConnectionData { Test = true }, new MutableServiceHub { CurrentUserController = new Mock { }.Object }).GetCurrentUser()); + + [TestMethod] + [AsyncStateMachine(typeof(UserTests))] + public Task TestRevocableSession() + { + IObjectState state = new MutableObjectState + { + ServerData = new Dictionary + { + ["sessionToken"] = "llaKcolnu" + } + }; + + IObjectState newState = new MutableObjectState + { + ServerData = new Dictionary + { + ["sessionToken"] = "r:llaKcolnu" + } + }; + + MutableServiceHub hub = new MutableServiceHub { }; + ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + ParseUser user = client.GenerateObjectFromState(state, "_User"); + + Mock mockSessionController = new Mock(); + mockSessionController.Setup(obj => obj.UpgradeToRevocableSessionAsync("llaKcolnu", It.IsAny(), It.IsAny())).Returns(Task.FromResult(newState)); + + hub.SessionController = mockSessionController.Object; + + return user.UpgradeToRevocableSessionAsync(CancellationToken.None).ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + + mockSessionController.Verify(obj => obj.UpgradeToRevocableSessionAsync("llaKcolnu", It.IsAny(), It.IsAny()), Times.Exactly(1)); + + Assert.AreEqual("r:llaKcolnu", user.SessionToken); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(UserTests))] + public Task TestRequestPasswordReset() + { + MutableServiceHub hub = new MutableServiceHub { }; + ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + Mock mockController = new Mock { }; + + hub.UserController = mockController.Object; + + return client.RequestPasswordResetAsync("gogo@parse.com").ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + + mockController.Verify(obj => obj.RequestPasswordResetAsync("gogo@parse.com", It.IsAny()), Times.Exactly(1)); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(UserTests))] + public Task TestUserSave() + { + IObjectState state = new MutableObjectState + { + ObjectId = "some0neTol4v4", + ServerData = new Dictionary + { + ["sessionToken"] = "llaKcolnu", + ["username"] = "ihave", + ["password"] = "adream" + } + }; + + IObjectState newState = new MutableObjectState + { + ServerData = new Dictionary + { + ["Alliance"] = "rekt" + } + }; + + MutableServiceHub hub = new MutableServiceHub { }; + ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + ParseUser user = client.GenerateObjectFromState(state, "_User"); + Mock mockObjectController = new Mock(); + + mockObjectController.Setup(obj => obj.SaveAsync(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(newState)); + + hub.ObjectController = mockObjectController.Object; + hub.CurrentUserController = new Mock { }.Object; + + user["Alliance"] = "rekt"; + + return user.SaveAsync().ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + + mockObjectController.Verify(obj => obj.SaveAsync(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); + + Assert.IsFalse(user.IsDirty); + Assert.AreEqual("ihave", user.Username); + Assert.IsFalse(user.State.ContainsKey("password")); + Assert.AreEqual("some0neTol4v4", user.ObjectId); + Assert.AreEqual("rekt", user["Alliance"]); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(UserTests))] + public Task TestUserFetch() + { + IObjectState state = new MutableObjectState + { + ObjectId = "some0neTol4v4", + ServerData = new Dictionary + { + ["sessionToken"] = "llaKcolnu", + ["username"] = "ihave", + ["password"] = "adream" + } + }; + + IObjectState newState = new MutableObjectState + { + ServerData = new Dictionary + { + ["Alliance"] = "rekt" + } + }; + + MutableServiceHub hub = new MutableServiceHub { }; + ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + ParseUser user = client.GenerateObjectFromState(state, "_User"); + + Mock mockObjectController = new Mock(); + mockObjectController.Setup(obj => obj.FetchAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(newState)); + + hub.ObjectController = mockObjectController.Object; + hub.CurrentUserController = new Mock { }.Object; + + user["Alliance"] = "rekt"; + + return user.FetchAsync().ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + + mockObjectController.Verify(obj => obj.FetchAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); + + Assert.IsTrue(user.IsDirty); + Assert.AreEqual("ihave", user.Username); + Assert.IsTrue(user.State.ContainsKey("password")); + Assert.AreEqual("some0neTol4v4", user.ObjectId); + Assert.AreEqual("rekt", user["Alliance"]); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(UserTests))] + public Task TestLink() + { + IObjectState state = new MutableObjectState + { + ObjectId = "some0neTol4v4", + ServerData = new Dictionary + { + ["sessionToken"] = "llaKcolnu" + } + }; + + IObjectState newState = new MutableObjectState + { + ServerData = new Dictionary + { + ["garden"] = "ofWords" + } + }; + + MutableServiceHub hub = new MutableServiceHub { }; + ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + ParseUser user = client.GenerateObjectFromState(state, "_User"); + + Mock mockObjectController = new Mock(); + mockObjectController.Setup(obj => obj.SaveAsync(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(newState)); + + hub.ObjectController = mockObjectController.Object; + hub.CurrentUserController = new Mock { }.Object; + + return user.LinkWithAsync("parse", new Dictionary { }, CancellationToken.None).ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + + mockObjectController.Verify(obj => obj.SaveAsync(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); + + Assert.IsFalse(user.IsDirty); + Assert.IsNotNull(user.AuthData); + Assert.IsNotNull(user.AuthData["parse"]); + Assert.AreEqual("some0neTol4v4", user.ObjectId); + Assert.AreEqual("ofWords", user["garden"]); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(UserTests))] + public Task TestUnlink() + { + IObjectState state = new MutableObjectState + { + ObjectId = "some0neTol4v4", + ServerData = new Dictionary + { + ["sessionToken"] = "llaKcolnu", + ["authData"] = new Dictionary + { + ["parse"] = new Dictionary { } + } + } + }; + + IObjectState newState = new MutableObjectState + { + ServerData = new Dictionary + { + ["garden"] = "ofWords" + } + }; + + MutableServiceHub hub = new MutableServiceHub { }; + ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + ParseUser user = client.GenerateObjectFromState(state, "_User"); + + Mock mockObjectController = new Mock(); + mockObjectController.Setup(obj => obj.SaveAsync(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(newState)); + + Mock mockCurrentUserController = new Mock { }; + mockCurrentUserController.Setup(obj => obj.IsCurrent(user)).Returns(true); + + hub.ObjectController = mockObjectController.Object; + hub.CurrentUserController = mockCurrentUserController.Object; + + return user.UnlinkFromAsync("parse", CancellationToken.None).ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + + mockObjectController.Verify(obj => obj.SaveAsync(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); + + Assert.IsFalse(user.IsDirty); + Assert.IsNotNull(user.AuthData); + Assert.IsFalse(user.AuthData.ContainsKey("parse")); + Assert.AreEqual("some0neTol4v4", user.ObjectId); + Assert.AreEqual("ofWords", user["garden"]); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(UserTests))] + public Task TestUnlinkNonCurrentUser() + { + IObjectState state = new MutableObjectState + { + ObjectId = "some0neTol4v4", + ServerData = new Dictionary + { + ["sessionToken"] = "llaKcolnu", + ["authData"] = new Dictionary + { + ["parse"] = new Dictionary { } + } + } + }; + + IObjectState newState = new MutableObjectState + { + ServerData = new Dictionary + { + ["garden"] = "ofWords" + } + }; + + MutableServiceHub hub = new MutableServiceHub { }; + ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + ParseUser user = client.GenerateObjectFromState(state, "_User"); + + Mock mockObjectController = new Mock(); + mockObjectController.Setup(obj => obj.SaveAsync(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(newState)); + + Mock mockCurrentUserController = new Mock { }; + mockCurrentUserController.Setup(obj => obj.IsCurrent(user)).Returns(false); + + hub.ObjectController = mockObjectController.Object; + hub.CurrentUserController = mockCurrentUserController.Object; + + return user.UnlinkFromAsync("parse", CancellationToken.None).ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + + mockObjectController.Verify(obj => obj.SaveAsync(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); + + Assert.IsFalse(user.IsDirty); + Assert.IsNotNull(user.AuthData); + Assert.IsTrue(user.AuthData.ContainsKey("parse")); + Assert.IsNull(user.AuthData["parse"]); + Assert.AreEqual("some0neTol4v4", user.ObjectId); + Assert.AreEqual("ofWords", user["garden"]); + }); + } + + [TestMethod] + [AsyncStateMachine(typeof(UserTests))] + public Task TestLogInWith() + { + IObjectState state = new MutableObjectState + { + ObjectId = "some0neTol4v4", + ServerData = new Dictionary + { + ["sessionToken"] = "llaKcolnu" + } + }; + + MutableServiceHub hub = new MutableServiceHub { }; + ParseClient client = new ParseClient(new ServerConnectionData { Test = true }, hub); + + Mock mockController = new Mock { }; + mockController.Setup(obj => obj.LogInAsync("parse", It.IsAny>(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(state)); + + hub.UserController = mockController.Object; + + return client.LogInWithAsync("parse", new Dictionary { }, CancellationToken.None).ContinueWith(task => + { + Assert.IsFalse(task.IsFaulted); + Assert.IsFalse(task.IsCanceled); + + mockController.Verify(obj => obj.LogInAsync("parse", It.IsAny>(), It.IsAny(), It.IsAny()), Times.Exactly(1)); + + ParseUser user = task.Result; + + Assert.IsNotNull(user.AuthData); + Assert.IsNotNull(user.AuthData["parse"]); + Assert.AreEqual("some0neTol4v4", user.ObjectId); + }); + } + + [TestMethod] + public void TestImmutableKeys() + { + ParseUser user = new ParseUser { }.Bind(Client) as ParseUser; + string[] immutableKeys = new string[] { "sessionToken", "isNew" }; + + foreach (string key in immutableKeys) + { + Assert.ThrowsException(() => user[key] = "1234567890"); + + Assert.ThrowsException(() => user.Add(key, "1234567890")); + + Assert.ThrowsException(() => user.AddRangeUniqueToList(key, new string[] { "1234567890" })); + + Assert.ThrowsException(() => user.Remove(key)); + + Assert.ThrowsException(() => user.RemoveAllFromList(key, new string[] { "1234567890" })); + } + + // Other special keys should be good. + + user["username"] = "username"; + user["password"] = "password"; + } + } +} diff --git a/Parse.sln b/Parse.sln index e7669198..cb26bb64 100644 --- a/Parse.sln +++ b/Parse.sln @@ -1,12 +1,10 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2036 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29905.134 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Parse", "Parse\Parse.csproj", "{297FE1CA-AEDF-47BB-964D-E82780EA0A1C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Parse.Test", "Parse.Test\Parse.Test.csproj", "{F54F1CF4-89AB-48E9-99C5-6CCD88B3EDBB}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{35AC7BF1-C7AA-4B48-A41D-858F226E660E}" ProjectSection(SolutionItems) = preProject .appveyor.yml = .appveyor.yml @@ -17,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Parse.Tests", "Parse.Tests\Parse.Tests.csproj", "{E5529694-B75B-4F07-8436-A749B5E801C3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,10 +27,10 @@ Global {297FE1CA-AEDF-47BB-964D-E82780EA0A1C}.Debug|Any CPU.Build.0 = Debug|Any CPU {297FE1CA-AEDF-47BB-964D-E82780EA0A1C}.Release|Any CPU.ActiveCfg = Release|Any CPU {297FE1CA-AEDF-47BB-964D-E82780EA0A1C}.Release|Any CPU.Build.0 = Release|Any CPU - {F54F1CF4-89AB-48E9-99C5-6CCD88B3EDBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F54F1CF4-89AB-48E9-99C5-6CCD88B3EDBB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F54F1CF4-89AB-48E9-99C5-6CCD88B3EDBB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F54F1CF4-89AB-48E9-99C5-6CCD88B3EDBB}.Release|Any CPU.Build.0 = Release|Any CPU + {E5529694-B75B-4F07-8436-A749B5E801C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5529694-B75B-4F07-8436-A749B5E801C3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5529694-B75B-4F07-8436-A749B5E801C3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5529694-B75B-4F07-8436-A749B5E801C3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Parse/Internal/Operation/IParseFieldOperation.cs b/Parse/Abstractions/Infrastructure/Control/IParseFieldOperation.cs similarity index 96% rename from Parse/Internal/Operation/IParseFieldOperation.cs rename to Parse/Abstractions/Infrastructure/Control/IParseFieldOperation.cs index 1dd018c2..102b0b98 100644 --- a/Parse/Internal/Operation/IParseFieldOperation.cs +++ b/Parse/Abstractions/Infrastructure/Control/IParseFieldOperation.cs @@ -1,6 +1,6 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. -namespace Parse.Core.Internal +namespace Parse.Abstractions.Infrastructure.Control { /// /// A ParseFieldOperation represents a modification to a value in a ParseObject. @@ -15,7 +15,7 @@ public interface IParseFieldOperation /// Parse as part of a save operation. /// /// An object to be JSONified. - object Encode(); + object Encode(IServiceHub serviceHub); /// /// Returns a field operation that is composed of a previous operation followed by diff --git a/Parse/Abstractions/Infrastructure/CustomServiceHub.cs b/Parse/Abstractions/Infrastructure/CustomServiceHub.cs new file mode 100644 index 00000000..49689c94 --- /dev/null +++ b/Parse/Abstractions/Infrastructure/CustomServiceHub.cs @@ -0,0 +1,66 @@ +using Parse.Abstractions.Infrastructure.Data; +using Parse.Abstractions.Infrastructure.Execution; +using Parse.Abstractions.Platform.Analytics; +using Parse.Abstractions.Platform.Cloud; +using Parse.Abstractions.Platform.Configuration; +using Parse.Abstractions.Platform.Files; +using Parse.Abstractions.Platform.Installations; +using Parse.Abstractions.Platform.Objects; +using Parse.Abstractions.Platform.Push; +using Parse.Abstractions.Platform.Queries; +using Parse.Abstractions.Platform.Sessions; +using Parse.Abstractions.Platform.Users; + +namespace Parse.Abstractions.Infrastructure +{ + public abstract class CustomServiceHub : ICustomServiceHub + { + public virtual IServiceHub Services { get; internal set; } + + public virtual IServiceHubCloner Cloner => Services.Cloner; + + public virtual IMetadataController MetadataController => Services.MetadataController; + + public virtual IWebClient WebClient => Services.WebClient; + + public virtual ICacheController CacheController => Services.CacheController; + + public virtual IParseObjectClassController ClassController => Services.ClassController; + + public virtual IParseInstallationController InstallationController => Services.InstallationController; + + public virtual IParseCommandRunner CommandRunner => Services.CommandRunner; + + public virtual IParseCloudCodeController CloudCodeController => Services.CloudCodeController; + + public virtual IParseConfigurationController ConfigurationController => Services.ConfigurationController; + + public virtual IParseFileController FileController => Services.FileController; + + public virtual IParseObjectController ObjectController => Services.ObjectController; + + public virtual IParseQueryController QueryController => Services.QueryController; + + public virtual IParseSessionController SessionController => Services.SessionController; + + public virtual IParseUserController UserController => Services.UserController; + + public virtual IParseCurrentUserController CurrentUserController => Services.CurrentUserController; + + public virtual IParseAnalyticsController AnalyticsController => Services.AnalyticsController; + + public virtual IParseInstallationCoder InstallationCoder => Services.InstallationCoder; + + public virtual IParsePushChannelsController PushChannelsController => Services.PushChannelsController; + + public virtual IParsePushController PushController => Services.PushController; + + public virtual IParseCurrentInstallationController CurrentInstallationController => Services.CurrentInstallationController; + + public virtual IServerConnectionData ServerConnectionData => Services.ServerConnectionData; + + public virtual IParseDataDecoder Decoder => Services.Decoder; + + public virtual IParseInstallationDataFinalizer InstallationDataFinalizer => Services.InstallationDataFinalizer; + } +} diff --git a/Parse/Abstractions/Infrastructure/Data/IParseDataDecoder.cs b/Parse/Abstractions/Infrastructure/Data/IParseDataDecoder.cs new file mode 100644 index 00000000..590cf169 --- /dev/null +++ b/Parse/Abstractions/Infrastructure/Data/IParseDataDecoder.cs @@ -0,0 +1,18 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +namespace Parse.Abstractions.Infrastructure.Data +{ + /// + /// A generalized input data decoding interface for the Parse SDK. + /// + public interface IParseDataDecoder + { + /// + /// Decodes input data into Parse-SDK-related entities, such as instances, which is why an implementation instance is sometimes required. + /// + /// The target input data to decode. + /// A implementation instance to use when instantiating s. + /// A Parse SDK entity such as a . + object Decode(object data, IServiceHub serviceHub); + } +} \ No newline at end of file diff --git a/Parse/Internal/Command/IParseCommandRunner.cs b/Parse/Abstractions/Infrastructure/Execution/IParseCommandRunner.cs similarity index 76% rename from Parse/Internal/Command/IParseCommandRunner.cs rename to Parse/Abstractions/Infrastructure/Execution/IParseCommandRunner.cs index 0739a45d..6718f003 100644 --- a/Parse/Internal/Command/IParseCommandRunner.cs +++ b/Parse/Abstractions/Infrastructure/Execution/IParseCommandRunner.cs @@ -5,8 +5,9 @@ using System.Net; using System.Threading; using System.Threading.Tasks; +using Parse.Infrastructure.Execution; -namespace Parse.Core.Internal +namespace Parse.Abstractions.Infrastructure.Execution { public interface IParseCommandRunner { @@ -18,9 +19,6 @@ public interface IParseCommandRunner /// Download progress callback. /// The cancellation token for the request. /// - Task>> RunCommandAsync(ParseCommand command, - IProgress uploadProgress = null, - IProgress downloadProgress = null, - CancellationToken cancellationToken = default(CancellationToken)); + Task>> RunCommandAsync(ParseCommand command, IProgress uploadProgress = null, IProgress downloadProgress = null, CancellationToken cancellationToken = default); } } diff --git a/Parse/Internal/HttpClient/IHttpClient.cs b/Parse/Abstractions/Infrastructure/Execution/IWebClient.cs similarity index 58% rename from Parse/Internal/HttpClient/IHttpClient.cs rename to Parse/Abstractions/Infrastructure/Execution/IWebClient.cs index 5d9a5cbd..db4f50ba 100644 --- a/Parse/Internal/HttpClient/IHttpClient.cs +++ b/Parse/Abstractions/Infrastructure/Execution/IWebClient.cs @@ -1,26 +1,24 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. using System; -using System.Net; using System.Threading; using System.Threading.Tasks; +using Parse.Infrastructure.Execution; +using Status = System.Net.HttpStatusCode; -namespace Parse.Common.Internal +namespace Parse.Abstractions.Infrastructure.Execution { - public interface IHttpClient + public interface IWebClient { /// - /// Executes HTTP request to a with HTTP verb - /// and . + /// Executes HTTP request to a with HTTP verb + /// and . /// /// The HTTP request to be executed. /// Upload progress callback. /// Download progress callback. /// The cancellation token. /// A task that resolves to Htt - Task> ExecuteAsync(HttpRequest httpRequest, - IProgress uploadProgress, - IProgress downloadProgress, - CancellationToken cancellationToken); + Task> ExecuteAsync(WebRequest httpRequest, IProgress uploadProgress, IProgress downloadProgress, CancellationToken cancellationToken = default); } } diff --git a/Parse/Abstractions/Infrastructure/ICacheController.cs b/Parse/Abstractions/Infrastructure/ICacheController.cs new file mode 100644 index 00000000..e3445deb --- /dev/null +++ b/Parse/Abstractions/Infrastructure/ICacheController.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace Parse.Abstractions.Infrastructure +{ + // TODO: Move TransferAsync to IDiskFileCacheController and find viable alternative for use in ICacheController if needed. + + /// + /// An abstraction for accessing persistent storage in the Parse SDK. + /// + public interface ICacheController + { + /// + /// Cleans up any temporary files and/or directories created during SDK operation. + /// + public void Clear(); + + /// + /// Gets the file wrapper for the specified . + /// + /// The relative path to the target file + /// An instance of wrapping the the value + FileInfo GetRelativeFile(string path); + + /// + /// Transfers a file from to . + /// + /// + /// + /// A task that completes once the file move operation form to completes. + Task TransferAsync(string originFilePath, string targetFilePath); + + /// + /// Load the contents of this storage controller asynchronously. + /// + /// + Task> LoadAsync(); + + /// + /// Overwrites the contents of this storage controller asynchronously. + /// + /// + /// + Task> SaveAsync(IDictionary contents); + } +} \ No newline at end of file diff --git a/Parse/Abstractions/Infrastructure/ICustomServiceHub.cs b/Parse/Abstractions/Infrastructure/ICustomServiceHub.cs new file mode 100644 index 00000000..967f77bc --- /dev/null +++ b/Parse/Abstractions/Infrastructure/ICustomServiceHub.cs @@ -0,0 +1,7 @@ +namespace Parse.Abstractions.Infrastructure +{ + public interface ICustomServiceHub : IServiceHub + { + IServiceHub Services { get; } + } +} diff --git a/Parse/Abstractions/Infrastructure/IDataCache.cs b/Parse/Abstractions/Infrastructure/IDataCache.cs new file mode 100644 index 00000000..047d489f --- /dev/null +++ b/Parse/Abstractions/Infrastructure/IDataCache.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Parse.Abstractions.Infrastructure +{ + // IGeneralizedDataCache + + /// + /// An interface for a dictionary that is persisted to disk asynchronously. + /// + /// They key type of the dictionary. + /// The value type of the dictionary. + public interface IDataCache : IDictionary + { + /// + /// Adds a key to this dictionary, and saves it asynchronously. + /// + /// The key to insert. + /// The value to insert. + /// + Task AddAsync(TKey key, TValue value); + + /// + /// Removes a key from this dictionary, and saves it asynchronously. + /// + /// + /// + Task RemoveAsync(TKey key); + } +} \ No newline at end of file diff --git a/Parse/Internal/Analytics/IParseAnalyticsPlugins.cs b/Parse/Abstractions/Infrastructure/IDataTransferLevel.cs similarity index 54% rename from Parse/Internal/Analytics/IParseAnalyticsPlugins.cs rename to Parse/Abstractions/Infrastructure/IDataTransferLevel.cs index 0e31876e..4257d1b4 100644 --- a/Parse/Internal/Analytics/IParseAnalyticsPlugins.cs +++ b/Parse/Abstractions/Infrastructure/IDataTransferLevel.cs @@ -1,15 +1,9 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. -using Parse.Core.Internal; -using System; - -namespace Parse.Analytics.Internal +namespace Parse.Abstractions.Infrastructure { - public interface IParseAnalyticsPlugins + public interface IDataTransferLevel { - void Reset(); - - IParseCorePlugins CorePlugins { get; } - IParseAnalyticsController AnalyticsController { get; } + double Amount { get; set; } } -} \ No newline at end of file +} diff --git a/Parse/Abstractions/Infrastructure/IDiskFileCacheController.cs b/Parse/Abstractions/Infrastructure/IDiskFileCacheController.cs new file mode 100644 index 00000000..dff5a045 --- /dev/null +++ b/Parse/Abstractions/Infrastructure/IDiskFileCacheController.cs @@ -0,0 +1,26 @@ +using System; + +namespace Parse.Abstractions.Infrastructure +{ + /// + /// An which stores the cache on disk via a file. + /// + public interface IDiskFileCacheController : ICacheController + { + /// + /// The path to a persistent user-specific storage location specific to the final client assembly of the Parse library. + /// + public string AbsoluteCacheFilePath { get; set; } + + /// + /// The relative path from the on the device an to application-specific persistent storage folder. + /// + public string RelativeCacheFilePath { get; set; } + + /// + /// Refreshes this cache controller's internal tracked cache file to reflect the and/or . + /// + /// This will not delete the active tracked cache file that will be un-tracked after a call to this method. To do so, call . + void RefreshPaths(); + } +} \ No newline at end of file diff --git a/Parse/Abstractions/Infrastructure/IEnvironmentData.cs b/Parse/Abstractions/Infrastructure/IEnvironmentData.cs new file mode 100644 index 00000000..14b454b5 --- /dev/null +++ b/Parse/Abstractions/Infrastructure/IEnvironmentData.cs @@ -0,0 +1,24 @@ +namespace Parse.Abstractions.Infrastructure +{ + /// + /// Information about the environment in which the library will be operating. + /// + public interface IEnvironmentData + { + /// + /// The currently active time zone when the library will be used. + /// + string TimeZone { get; } + + /// + /// The operating system version of the platform the SDK is operating in. + /// + string OSVersion { get; } + + /// + /// An identifier of the platform. + /// + /// Expected to be one of ios, android, winrt, winphone, or dotnet. + public string Platform { get; set; } + } +} diff --git a/Parse/Abstractions/Infrastructure/IHostManifestData.cs b/Parse/Abstractions/Infrastructure/IHostManifestData.cs new file mode 100644 index 00000000..b8f2c5f7 --- /dev/null +++ b/Parse/Abstractions/Infrastructure/IHostManifestData.cs @@ -0,0 +1,28 @@ +namespace Parse.Abstractions.Infrastructure +{ + /// + /// Information about the application using the Parse SDK. + /// + public interface IHostManifestData + { + /// + /// The build number of your app. + /// + string Version { get; } + + /// + /// The human friendly version number of your app. + /// + string ShortVersion { get; } + + /// + /// A unique string representing your app. + /// + string Identifier { get; } + + /// + /// The name of your app. + /// + string Name { get; } + } +} diff --git a/Parse/Internal/Utilities/IJsonConvertible.cs b/Parse/Abstractions/Infrastructure/IJsonConvertible.cs similarity index 87% rename from Parse/Internal/Utilities/IJsonConvertible.cs rename to Parse/Abstractions/Infrastructure/IJsonConvertible.cs index 0a9f1aff..6071d8bb 100644 --- a/Parse/Internal/Utilities/IJsonConvertible.cs +++ b/Parse/Abstractions/Infrastructure/IJsonConvertible.cs @@ -1,9 +1,8 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. -using System; using System.Collections.Generic; -namespace Parse.Common.Internal +namespace Parse.Abstractions.Infrastructure { /// /// Represents an object that can be converted into JSON. @@ -14,6 +13,6 @@ public interface IJsonConvertible /// Converts the object to a data structure that can be converted to JSON. /// /// An object to be JSONified. - IDictionary ToJSON(); + IDictionary ConvertToJSON(); } } diff --git a/Parse/Abstractions/Infrastructure/IMetadataController.cs b/Parse/Abstractions/Infrastructure/IMetadataController.cs new file mode 100644 index 00000000..90332138 --- /dev/null +++ b/Parse/Abstractions/Infrastructure/IMetadataController.cs @@ -0,0 +1,19 @@ +namespace Parse.Abstractions.Infrastructure +{ + /// + /// A controller for metadata. This is provided in a dependency injection container because if a beta feature is activated for a client managing a specific aspect of application operation, then this might need to be reflected in the application versioning information as it is used to determine the data cache location. + /// + /// This container could have been implemented as a or , due to it's simplicity but, more information may be added in the future so it is kept general. + public interface IMetadataController + { + /// + /// Information about the application using the Parse SDK. + /// + public IHostManifestData HostManifestData { get; } + + /// + /// Environment data specific to the application hosting the Parse SDK. + /// + public IEnvironmentData EnvironmentData { get; } + } +} diff --git a/Parse/Abstractions/Infrastructure/IMutableServiceHub.cs b/Parse/Abstractions/Infrastructure/IMutableServiceHub.cs new file mode 100644 index 00000000..7282985f --- /dev/null +++ b/Parse/Abstractions/Infrastructure/IMutableServiceHub.cs @@ -0,0 +1,52 @@ +#pragma warning disable CS0108 // Member hides inherited member; missing new keyword + +using Parse.Abstractions.Infrastructure.Data; +using Parse.Abstractions.Infrastructure.Execution; +using Parse.Abstractions.Platform.Analytics; +using Parse.Abstractions.Platform.Cloud; +using Parse.Abstractions.Platform.Configuration; +using Parse.Abstractions.Platform.Files; +using Parse.Abstractions.Platform.Installations; +using Parse.Abstractions.Platform.Objects; +using Parse.Abstractions.Platform.Push; +using Parse.Abstractions.Platform.Queries; +using Parse.Abstractions.Platform.Sessions; +using Parse.Abstractions.Platform.Users; + +namespace Parse.Abstractions.Infrastructure +{ + public interface IMutableServiceHub : IServiceHub + { + IServerConnectionData ServerConnectionData { set; } + IMetadataController MetadataController { set; } + + IServiceHubCloner Cloner { set; } + + IWebClient WebClient { set; } + ICacheController CacheController { set; } + IParseObjectClassController ClassController { set; } + + IParseDataDecoder Decoder { set; } + + IParseInstallationController InstallationController { set; } + IParseCommandRunner CommandRunner { set; } + + IParseCloudCodeController CloudCodeController { set; } + IParseConfigurationController ConfigurationController { set; } + IParseFileController FileController { set; } + IParseObjectController ObjectController { set; } + IParseQueryController QueryController { set; } + IParseSessionController SessionController { set; } + IParseUserController UserController { set; } + IParseCurrentUserController CurrentUserController { set; } + + IParseAnalyticsController AnalyticsController { set; } + + IParseInstallationCoder InstallationCoder { set; } + + IParsePushChannelsController PushChannelsController { set; } + IParsePushController PushController { set; } + IParseCurrentInstallationController CurrentInstallationController { set; } + IParseInstallationDataFinalizer InstallationDataFinalizer { set; } + } +} diff --git a/Parse/Abstractions/Infrastructure/IRelativeCacheLocationGenerator.cs b/Parse/Abstractions/Infrastructure/IRelativeCacheLocationGenerator.cs new file mode 100644 index 00000000..7ceabe26 --- /dev/null +++ b/Parse/Abstractions/Infrastructure/IRelativeCacheLocationGenerator.cs @@ -0,0 +1,13 @@ +namespace Parse.Abstractions.Infrastructure +{ + /// + /// A unit that can generate a relative path to a persistent storage file. + /// + public interface IRelativeCacheLocationGenerator + { + /// + /// The corresponding relative path generated by this . + /// + string GetRelativeCacheFilePath(IServiceHub serviceHub); + } +} diff --git a/Parse/Abstractions/Infrastructure/IServerConnectionData.cs b/Parse/Abstractions/Infrastructure/IServerConnectionData.cs new file mode 100644 index 00000000..bad1c01d --- /dev/null +++ b/Parse/Abstractions/Infrastructure/IServerConnectionData.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +namespace Parse.Abstractions.Infrastructure +{ + public interface IServerConnectionData + { + /// + /// The App ID of your app. + /// + string ApplicationID { get; set; } + + /// + /// A URI pointing to the target Parse Server instance hosting the app targeted by . + /// + string ServerURI { get; set; } + + /// + /// The .NET Key for the Parse app targeted by . + /// + string Key { get; set; } + + /// + /// The Master Key for the Parse app targeted by . + /// + string MasterKey { get; set; } + + /// + /// Additional HTTP headers to be sent with network requests from the SDK. + /// + IDictionary Headers { get; set; } + } +} diff --git a/Parse/Abstractions/Infrastructure/IServiceHub.cs b/Parse/Abstractions/Infrastructure/IServiceHub.cs new file mode 100644 index 00000000..c7e65992 --- /dev/null +++ b/Parse/Abstractions/Infrastructure/IServiceHub.cs @@ -0,0 +1,60 @@ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +using Parse.Abstractions.Infrastructure.Data; +using Parse.Abstractions.Infrastructure.Execution; +using Parse.Abstractions.Platform.Analytics; +using Parse.Abstractions.Platform.Cloud; +using Parse.Abstractions.Platform.Configuration; +using Parse.Abstractions.Platform.Files; +using Parse.Abstractions.Platform.Installations; +using Parse.Abstractions.Platform.Objects; +using Parse.Abstractions.Platform.Push; +using Parse.Abstractions.Platform.Queries; +using Parse.Abstractions.Platform.Sessions; +using Parse.Abstractions.Platform.Users; + +namespace Parse.Abstractions.Infrastructure +{ + // TODO: Consider splitting up IServiceHub into IResourceHub and IServiceHub, where the former would provide the current functionality of IServiceHub and the latter would be a public-facing sub-section containing formerly-static memebers from classes such as ParseObject which require the use of some broader resource. + + /// + /// The dependency injection container for all internal .NET Parse SDK services. + /// + public interface IServiceHub + { + /// + /// The current server connection data that the the Parse SDK has been initialized with. + /// + IServerConnectionData ServerConnectionData { get; } + IMetadataController MetadataController { get; } + + IServiceHubCloner Cloner { get; } + + IWebClient WebClient { get; } + ICacheController CacheController { get; } + IParseObjectClassController ClassController { get; } + + IParseDataDecoder Decoder { get; } + + IParseInstallationController InstallationController { get; } + IParseCommandRunner CommandRunner { get; } + + IParseCloudCodeController CloudCodeController { get; } + IParseConfigurationController ConfigurationController { get; } + IParseFileController FileController { get; } + IParseObjectController ObjectController { get; } + IParseQueryController QueryController { get; } + IParseSessionController SessionController { get; } + IParseUserController UserController { get; } + IParseCurrentUserController CurrentUserController { get; } + + IParseAnalyticsController AnalyticsController { get; } + + IParseInstallationCoder InstallationCoder { get; } + + IParsePushChannelsController PushChannelsController { get; } + IParsePushController PushController { get; } + IParseCurrentInstallationController CurrentInstallationController { get; } + IParseInstallationDataFinalizer InstallationDataFinalizer { get; } + } +} diff --git a/Parse/Abstractions/Infrastructure/IServiceHubCloner.cs b/Parse/Abstractions/Infrastructure/IServiceHubCloner.cs new file mode 100644 index 00000000..d88dea9e --- /dev/null +++ b/Parse/Abstractions/Infrastructure/IServiceHubCloner.cs @@ -0,0 +1,7 @@ +namespace Parse.Abstractions.Infrastructure +{ + public interface IServiceHubCloner + { + public IServiceHub BuildHub(in IServiceHub reference, IServiceHubComposer composer, params IServiceHubMutator[] requestedMutators); + } +} diff --git a/Parse/Abstractions/Infrastructure/IServiceHubComposer.cs b/Parse/Abstractions/Infrastructure/IServiceHubComposer.cs new file mode 100644 index 00000000..73dc3597 --- /dev/null +++ b/Parse/Abstractions/Infrastructure/IServiceHubComposer.cs @@ -0,0 +1,9 @@ +namespace Parse.Abstractions.Infrastructure +{ + // ALTERNATE NAME: IClient, IDataContainmentHub, IResourceContainmentHub, IDataContainer, IServiceHubComposer + + public interface IServiceHubComposer + { + public IServiceHub BuildHub(IMutableServiceHub serviceHub = default, IServiceHub extension = default, params IServiceHubMutator[] configurators); + } +} diff --git a/Parse/Abstractions/Infrastructure/IServiceHubMutator.cs b/Parse/Abstractions/Infrastructure/IServiceHubMutator.cs new file mode 100644 index 00000000..6ce77d67 --- /dev/null +++ b/Parse/Abstractions/Infrastructure/IServiceHubMutator.cs @@ -0,0 +1,22 @@ +namespace Parse.Abstractions.Infrastructure +{ + // IServiceHubComposer, IServiceHubMutator, IServiceHubConfigurator, IClientConfigurator, IServiceConfigurationLayer + + /// + /// A class which makes a deliberate mutation to a service. + /// + public interface IServiceHubMutator + { + /// + /// A value which dictates whether or not the should be considered in a valid state. + /// + bool Valid { get; } + + /// + /// A method which mutates an implementation instance. + /// + /// The target implementation instance + /// A hub which the is composed onto that should be used when needs to access services. + void Mutate(ref IMutableServiceHub target, in IServiceHub composedHub); + } +} diff --git a/Parse/Internal/Analytics/Controller/IParseAnalyticsController.cs b/Parse/Abstractions/Platform/Analytics/IParseAnalyticsController.cs similarity index 79% rename from Parse/Internal/Analytics/Controller/IParseAnalyticsController.cs rename to Parse/Abstractions/Platform/Analytics/IParseAnalyticsController.cs index 334abad3..250d9ad9 100644 --- a/Parse/Internal/Analytics/Controller/IParseAnalyticsController.cs +++ b/Parse/Abstractions/Platform/Analytics/IParseAnalyticsController.cs @@ -3,9 +3,9 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure; - -namespace Parse.Analytics.Internal +namespace Parse.Abstractions.Platform.Analytics { /// /// The interface for the Parse Analytics API controller. @@ -20,10 +20,7 @@ public interface IParseAnalyticsController /// The session token for the event. /// The asynchonous cancellation token. /// A that will complete successfully once the event has been set to be tracked. - Task TrackEventAsync(string name, - IDictionary dimensions, - string sessionToken, - CancellationToken cancellationToken); + Task TrackEventAsync(string name, IDictionary dimensions, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); /// /// Tracks an app open for the specified event. @@ -32,8 +29,6 @@ Task TrackEventAsync(string name, /// The token of the current session. /// The asynchronous cancellation token. /// A the will complete successfully once app openings for the target push notification have been set to be tracked. - Task TrackAppOpenedAsync(string pushHash, - string sessionToken, - CancellationToken cancellationToken); + Task TrackAppOpenedAsync(string pushHash, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); } } diff --git a/Parse/Internal/Authentication/IParseAuthenticationProvider.cs b/Parse/Abstractions/Platform/Authentication/IParseAuthenticationProvider.cs similarity index 97% rename from Parse/Internal/Authentication/IParseAuthenticationProvider.cs rename to Parse/Abstractions/Platform/Authentication/IParseAuthenticationProvider.cs index 96e4145f..d177ee9d 100644 --- a/Parse/Internal/Authentication/IParseAuthenticationProvider.cs +++ b/Parse/Abstractions/Platform/Authentication/IParseAuthenticationProvider.cs @@ -4,7 +4,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Parse.Core.Internal +namespace Parse.Abstractions.Platform.Authentication { public interface IParseAuthenticationProvider { diff --git a/Parse/Internal/Cloud/Controller/IParseCloudCodeController.cs b/Parse/Abstractions/Platform/Cloud/IParseCloudCodeController.cs similarity index 62% rename from Parse/Internal/Cloud/Controller/IParseCloudCodeController.cs rename to Parse/Abstractions/Platform/Cloud/IParseCloudCodeController.cs index bc4d237d..a816ad2e 100644 --- a/Parse/Internal/Cloud/Controller/IParseCloudCodeController.cs +++ b/Parse/Abstractions/Platform/Cloud/IParseCloudCodeController.cs @@ -1,17 +1,14 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. -using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure; -namespace Parse.Core.Internal +namespace Parse.Abstractions.Platform.Cloud { public interface IParseCloudCodeController { - Task CallFunctionAsync(string name, - IDictionary parameters, - string sessionToken, - CancellationToken cancellationToken); + Task CallFunctionAsync(string name, IDictionary parameters, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); } } diff --git a/Parse/Internal/Config/Controller/IParseConfigController.cs b/Parse/Abstractions/Platform/Configuration/IParseConfigurationController.cs similarity index 61% rename from Parse/Internal/Config/Controller/IParseConfigController.cs rename to Parse/Abstractions/Platform/Configuration/IParseConfigurationController.cs index f3051eb5..58fb0247 100644 --- a/Parse/Internal/Config/Controller/IParseConfigController.cs +++ b/Parse/Abstractions/Platform/Configuration/IParseConfigurationController.cs @@ -1,18 +1,15 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. -using System; -using System.Threading.Tasks; using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure; +using Parse.Platform.Configuration; -namespace Parse.Core.Internal +namespace Parse.Abstractions.Platform.Configuration { - public interface IParseConfigController + public interface IParseConfigurationController { - /// - /// Gets the current config controller. - /// - /// The current config controller. - IParseCurrentConfigController CurrentConfigController { get; } + public IParseCurrentConfigurationController CurrentConfigurationController { get; } /// /// Fetches the config from the server asynchronously. @@ -20,6 +17,6 @@ public interface IParseConfigController /// The config async. /// Session token. /// Cancellation token. - Task FetchConfigAsync(string sessionToken, CancellationToken cancellationToken); + Task FetchConfigAsync(string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); } } diff --git a/Parse/Internal/Config/Controller/IParseCurrentConfigController.cs b/Parse/Abstractions/Platform/Configuration/IParseCurrentConfigurationController.cs similarity index 76% rename from Parse/Internal/Config/Controller/IParseCurrentConfigController.cs rename to Parse/Abstractions/Platform/Configuration/IParseCurrentConfigurationController.cs index 307e431e..427073c8 100644 --- a/Parse/Internal/Config/Controller/IParseCurrentConfigController.cs +++ b/Parse/Abstractions/Platform/Configuration/IParseCurrentConfigurationController.cs @@ -1,24 +1,25 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. -using System; using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure; +using Parse.Platform.Configuration; -namespace Parse.Core.Internal +namespace Parse.Abstractions.Platform.Configuration { - public interface IParseCurrentConfigController + public interface IParseCurrentConfigurationController { /// /// Gets the current config async. /// /// The current config async. - Task GetCurrentConfigAsync(); + Task GetCurrentConfigAsync(IServiceHub serviceHub); /// /// Sets the current config async. /// /// The current config async. /// Config. - Task SetCurrentConfigAsync(ParseConfig config); + Task SetCurrentConfigAsync(ParseConfiguration config); /// /// Clears the current config async. diff --git a/Parse/Internal/File/Controller/IParseFileController.cs b/Parse/Abstractions/Platform/Files/IParseFileController.cs similarity index 60% rename from Parse/Internal/File/Controller/IParseFileController.cs rename to Parse/Abstractions/Platform/Files/IParseFileController.cs index f2e1e6a2..bfe1a117 100644 --- a/Parse/Internal/File/Controller/IParseFileController.cs +++ b/Parse/Abstractions/Platform/Files/IParseFileController.cs @@ -4,15 +4,13 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure; +using Parse.Platform.Files; -namespace Parse.Core.Internal +namespace Parse.Abstractions.Platform.Files { public interface IParseFileController { - Task SaveAsync(FileState state, - Stream dataStream, - string sessionToken, - IProgress progress, - CancellationToken cancellationToken); + Task SaveAsync(FileState state, Stream dataStream, string sessionToken, IProgress progress, CancellationToken cancellationToken); } } diff --git a/Parse/Internal/Push/Installation/Controller/IParseCurrentInstallationController.cs b/Parse/Abstractions/Platform/Installations/IParseCurrentInstallationController.cs similarity index 81% rename from Parse/Internal/Push/Installation/Controller/IParseCurrentInstallationController.cs rename to Parse/Abstractions/Platform/Installations/IParseCurrentInstallationController.cs index c8d6fb0a..77b73077 100644 --- a/Parse/Internal/Push/Installation/Controller/IParseCurrentInstallationController.cs +++ b/Parse/Abstractions/Platform/Installations/IParseCurrentInstallationController.cs @@ -1,9 +1,8 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. -using Parse.Core.Internal; -using System; +using Parse.Abstractions.Platform.Objects; -namespace Parse.Push.Internal +namespace Parse.Abstractions.Platform.Installations { public interface IParseCurrentInstallationController : IParseObjectCurrentController { diff --git a/Parse/Internal/Push/Installation/Coder/IParseInstallationCoder.cs b/Parse/Abstractions/Platform/Installations/IParseInstallationCoder.cs similarity index 72% rename from Parse/Internal/Push/Installation/Coder/IParseInstallationCoder.cs rename to Parse/Abstractions/Platform/Installations/IParseInstallationCoder.cs index a1166ded..0db3b683 100644 --- a/Parse/Internal/Push/Installation/Coder/IParseInstallationCoder.cs +++ b/Parse/Abstractions/Platform/Installations/IParseInstallationCoder.cs @@ -1,14 +1,14 @@ -using System; using System.Collections.Generic; -using Parse; +using Parse.Abstractions.Infrastructure; -namespace Parse.Push.Internal +namespace Parse.Abstractions.Platform.Installations { // TODO: (richardross) once coder is refactored, make this extend IParseObjectCoder. + public interface IParseInstallationCoder { IDictionary Encode(ParseInstallation installation); - ParseInstallation Decode(IDictionary data); + ParseInstallation Decode(IDictionary data, IServiceHub serviceHub); } } \ No newline at end of file diff --git a/Parse/Internal/InstallationId/Controller/IInstallationIdController.cs b/Parse/Abstractions/Platform/Installations/IParseInstallationController.cs similarity index 90% rename from Parse/Internal/InstallationId/Controller/IInstallationIdController.cs rename to Parse/Abstractions/Platform/Installations/IParseInstallationController.cs index 1cdf27ea..1fc9f1b4 100644 --- a/Parse/Internal/InstallationId/Controller/IInstallationIdController.cs +++ b/Parse/Abstractions/Platform/Installations/IParseInstallationController.cs @@ -1,12 +1,11 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. using System; -using System.Threading; using System.Threading.Tasks; -namespace Parse.Core.Internal +namespace Parse.Abstractions.Platform.Installations { - public interface IInstallationIdController + public interface IParseInstallationController { /// /// Sets current installationId and saves it to local storage. diff --git a/Parse/Abstractions/Platform/Installations/IParseInstallationDataFinalizer.cs b/Parse/Abstractions/Platform/Installations/IParseInstallationDataFinalizer.cs new file mode 100644 index 00000000..9296ffef --- /dev/null +++ b/Parse/Abstractions/Platform/Installations/IParseInstallationDataFinalizer.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; + +namespace Parse.Abstractions.Platform.Installations +{ + public interface IParseInstallationDataFinalizer + { + /// + /// Executes platform specific hook that mutate the installation based on + /// the device platforms. + /// + /// Installation to be mutated. + /// + Task FinalizeAsync(ParseInstallation installation); + + /// + /// Allows an implementation to get static information that needs to be used in . + /// + void Initialize(); + } +} diff --git a/Parse/Internal/Object/State/IObjectState.cs b/Parse/Abstractions/Platform/Objects/IObjectState.cs similarity index 90% rename from Parse/Internal/Object/State/IObjectState.cs rename to Parse/Abstractions/Platform/Objects/IObjectState.cs index 473f4428..1da67c59 100644 --- a/Parse/Internal/Object/State/IObjectState.cs +++ b/Parse/Abstractions/Platform/Objects/IObjectState.cs @@ -2,8 +2,9 @@ using System; using System.Collections.Generic; +using Parse.Platform.Objects; -namespace Parse.Core.Internal +namespace Parse.Abstractions.Platform.Objects { public interface IObjectState : IEnumerable> { diff --git a/Parse/Abstractions/Platform/Objects/IParseObjectClassController.cs b/Parse/Abstractions/Platform/Objects/IParseObjectClassController.cs new file mode 100644 index 00000000..07ad585c --- /dev/null +++ b/Parse/Abstractions/Platform/Objects/IParseObjectClassController.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using Parse.Abstractions.Infrastructure; + +namespace Parse.Abstractions.Platform.Objects +{ + public interface IParseObjectClassController + { + string GetClassName(Type type); + + Type GetType(string className); + + bool GetClassMatch(string className, Type type); + + void AddValid(Type type); + + void RemoveClass(Type type); + + void AddRegisterHook(Type type, Action action); + + ParseObject Instantiate(string className, IServiceHub serviceHub); + + IDictionary GetPropertyMappings(string className); + + void AddIntrinsic(); + } +} diff --git a/Parse/Internal/Object/Controller/IParseObjectController.cs b/Parse/Abstractions/Platform/Objects/IParseObjectController.cs similarity index 62% rename from Parse/Internal/Object/Controller/IParseObjectController.cs rename to Parse/Abstractions/Platform/Objects/IParseObjectController.cs index 0dd2363d..a17bfd14 100644 --- a/Parse/Internal/Object/Controller/IParseObjectController.cs +++ b/Parse/Abstractions/Platform/Objects/IParseObjectController.cs @@ -1,22 +1,23 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. -using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure.Control; +using Parse.Abstractions.Infrastructure; -namespace Parse.Core.Internal +namespace Parse.Abstractions.Platform.Objects { public interface IParseObjectController { - Task FetchAsync(IObjectState state, string sessionToken, CancellationToken cancellationToken); + Task FetchAsync(IObjectState state, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); - Task SaveAsync(IObjectState state, IDictionary operations, string sessionToken, CancellationToken cancellationToken); + Task SaveAsync(IObjectState state, IDictionary operations, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); - IList> SaveAllAsync(IList states, IList> operationsList, string sessionToken, CancellationToken cancellationToken); + IList> SaveAllAsync(IList states, IList> operationsList, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); - Task DeleteAsync(IObjectState state, string sessionToken, CancellationToken cancellationToken); + Task DeleteAsync(IObjectState state, string sessionToken, CancellationToken cancellationToken = default); - IList DeleteAllAsync(IList states, string sessionToken, CancellationToken cancellationToken); + IList DeleteAllAsync(IList states, string sessionToken, CancellationToken cancellationToken = default); } } diff --git a/Parse/Internal/Object/Controller/IParseObjectCurrentController.cs b/Parse/Abstractions/Platform/Objects/IParseObjectCurrentController.cs similarity index 91% rename from Parse/Internal/Object/Controller/IParseObjectCurrentController.cs rename to Parse/Abstractions/Platform/Objects/IParseObjectCurrentController.cs index eb505108..55418651 100644 --- a/Parse/Internal/Object/Controller/IParseObjectCurrentController.cs +++ b/Parse/Abstractions/Platform/Objects/IParseObjectCurrentController.cs @@ -1,10 +1,10 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. -using System; using System.Threading; using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure; -namespace Parse.Core.Internal +namespace Parse.Abstractions.Platform.Objects { /// /// IParseObjectCurrentController controls the single-instance @@ -19,20 +19,20 @@ public interface IParseObjectCurrentController where T : ParseObject /// /// to be persisted. /// The cancellation token. - Task SetAsync(T obj, CancellationToken cancellationToken); + Task SetAsync(T obj, CancellationToken cancellationToken = default); /// /// Gets the persisted current . /// /// The cancellation token. - Task GetAsync(CancellationToken cancellationToken); + Task GetAsync(IServiceHub serviceHub, CancellationToken cancellationToken = default); /// /// Returns a that resolves to true if current /// exists. /// /// The cancellation token. - Task ExistsAsync(CancellationToken cancellationToken); + Task ExistsAsync(CancellationToken cancellationToken = default); /// /// Returns true if the given is the persisted current diff --git a/Parse/Internal/Push/Controller/IParsePushChannelsController.cs b/Parse/Abstractions/Platform/Push/IParsePushChannelsController.cs similarity index 57% rename from Parse/Internal/Push/Controller/IParsePushChannelsController.cs rename to Parse/Abstractions/Platform/Push/IParsePushChannelsController.cs index 9ed55bdb..e24829b8 100644 --- a/Parse/Internal/Push/Controller/IParsePushChannelsController.cs +++ b/Parse/Abstractions/Platform/Push/IParsePushChannelsController.cs @@ -1,16 +1,16 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. -using System; -using System.Threading.Tasks; -using System.Collections; -using System.Threading; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure; -namespace Parse.Push.Internal +namespace Parse.Abstractions.Platform.Push { public interface IParsePushChannelsController { - Task SubscribeAsync(IEnumerable channels, CancellationToken cancellationToken); - Task UnsubscribeAsync(IEnumerable channels, CancellationToken cancellationToken); + Task SubscribeAsync(IEnumerable channels, IServiceHub serviceHub, CancellationToken cancellationToken); + + Task UnsubscribeAsync(IEnumerable channels, IServiceHub serviceHub, CancellationToken cancellationToken); } } diff --git a/Parse/Internal/Push/Controller/IParsePushController.cs b/Parse/Abstractions/Platform/Push/IParsePushController.cs similarity index 65% rename from Parse/Internal/Push/Controller/IParsePushController.cs rename to Parse/Abstractions/Platform/Push/IParsePushController.cs index 14b1569e..c5c5e133 100644 --- a/Parse/Internal/Push/Controller/IParsePushController.cs +++ b/Parse/Abstractions/Platform/Push/IParsePushController.cs @@ -1,13 +1,13 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. -using System; -using System.Threading.Tasks; using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure; -namespace Parse.Push.Internal +namespace Parse.Abstractions.Platform.Push { public interface IParsePushController { - Task SendPushNotificationAsync(IPushState state, CancellationToken cancellationToken); + Task SendPushNotificationAsync(IPushState state, IServiceHub serviceHub, CancellationToken cancellationToken = default); } } diff --git a/Parse/Internal/Push/State/IPushState.cs b/Parse/Abstractions/Platform/Push/IPushState.cs similarity index 91% rename from Parse/Internal/Push/State/IPushState.cs rename to Parse/Abstractions/Platform/Push/IPushState.cs index 4b8fd9b4..e62a16b0 100644 --- a/Parse/Internal/Push/State/IPushState.cs +++ b/Parse/Abstractions/Platform/Push/IPushState.cs @@ -2,8 +2,9 @@ using System; using System.Collections.Generic; +using Parse.Platform.Push; -namespace Parse.Push.Internal +namespace Parse.Abstractions.Platform.Push { public interface IPushState { diff --git a/Parse/Internal/Query/Controller/IParseQueryController.cs b/Parse/Abstractions/Platform/Queries/IParseQueryController.cs similarity index 64% rename from Parse/Internal/Query/Controller/IParseQueryController.cs rename to Parse/Abstractions/Platform/Queries/IParseQueryController.cs index 7a490ac0..0e174fea 100644 --- a/Parse/Internal/Query/Controller/IParseQueryController.cs +++ b/Parse/Abstractions/Platform/Queries/IParseQueryController.cs @@ -1,19 +1,18 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. -using System; -using System.Linq; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Parse.Abstractions.Platform.Objects; -namespace Parse.Core.Internal +namespace Parse.Abstractions.Platform.Queries { public interface IParseQueryController { - Task> FindAsync(ParseQuery query, ParseUser user, CancellationToken cancellationToken) where T : ParseObject; + Task> FindAsync(ParseQuery query, ParseUser user, CancellationToken cancellationToken = default) where T : ParseObject; - Task CountAsync(ParseQuery query, ParseUser user, CancellationToken cancellationToken) where T : ParseObject; + Task CountAsync(ParseQuery query, ParseUser user, CancellationToken cancellationToken = default) where T : ParseObject; - Task FirstAsync(ParseQuery query, ParseUser user, CancellationToken cancellationToken) where T : ParseObject; + Task FirstAsync(ParseQuery query, ParseUser user, CancellationToken cancellationToken = default) where T : ParseObject; } } diff --git a/Parse/Internal/Session/Controller/IParseSessionController.cs b/Parse/Abstractions/Platform/Sessions/IParseSessionController.cs similarity index 67% rename from Parse/Internal/Session/Controller/IParseSessionController.cs rename to Parse/Abstractions/Platform/Sessions/IParseSessionController.cs index a7f75b27..ab6016e0 100644 --- a/Parse/Internal/Session/Controller/IParseSessionController.cs +++ b/Parse/Abstractions/Platform/Sessions/IParseSessionController.cs @@ -1,18 +1,19 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. -using System; using System.Threading; using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Platform.Objects; -namespace Parse.Core.Internal +namespace Parse.Abstractions.Platform.Sessions { public interface IParseSessionController { - Task GetSessionAsync(string sessionToken, CancellationToken cancellationToken); + Task GetSessionAsync(string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); - Task RevokeAsync(string sessionToken, CancellationToken cancellationToken); + Task RevokeAsync(string sessionToken, CancellationToken cancellationToken = default); - Task UpgradeToRevocableSessionAsync(string sessionToken, CancellationToken cancellationToken); + Task UpgradeToRevocableSessionAsync(string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); bool IsRevocableSessionToken(string sessionToken); } diff --git a/Parse/Internal/User/Controller/IParseCurrentUserController.cs b/Parse/Abstractions/Platform/Users/IParseCurrentUserController.cs similarity index 56% rename from Parse/Internal/User/Controller/IParseCurrentUserController.cs rename to Parse/Abstractions/Platform/Users/IParseCurrentUserController.cs index d235c480..5b6b50a5 100644 --- a/Parse/Internal/User/Controller/IParseCurrentUserController.cs +++ b/Parse/Abstractions/Platform/Users/IParseCurrentUserController.cs @@ -1,15 +1,16 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. -using System; using System.Threading; using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Platform.Objects; -namespace Parse.Core.Internal +namespace Parse.Abstractions.Platform.Users { public interface IParseCurrentUserController : IParseObjectCurrentController { - Task GetCurrentSessionTokenAsync(CancellationToken cancellationToken); + Task GetCurrentSessionTokenAsync(IServiceHub serviceHub, CancellationToken cancellationToken = default); - Task LogOutAsync(CancellationToken cancellationToken); + Task LogOutAsync(IServiceHub serviceHub, CancellationToken cancellationToken = default); } } diff --git a/Parse/Abstractions/Platform/Users/IParseUserController.cs b/Parse/Abstractions/Platform/Users/IParseUserController.cs new file mode 100644 index 00000000..11cd0217 --- /dev/null +++ b/Parse/Abstractions/Platform/Users/IParseUserController.cs @@ -0,0 +1,28 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure.Control; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Platform.Objects; + +namespace Parse.Abstractions.Platform.Users +{ + public interface IParseUserController + { + Task SignUpAsync(IObjectState state, IDictionary operations, IServiceHub serviceHub, CancellationToken cancellationToken = default); + + Task LogInAsync(string username, string password, IServiceHub serviceHub, CancellationToken cancellationToken = default); + + Task LogInAsync(string authType, IDictionary data, IServiceHub serviceHub, CancellationToken cancellationToken = default); + + Task GetUserAsync(string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default); + + Task RequestPasswordResetAsync(string email, CancellationToken cancellationToken = default); + + bool RevocableSessionEnabled { get; set; } + + object RevocableSessionEnabledMutex { get; } + } +} diff --git a/Parse/Infrastructure/AbsoluteCacheLocationMutator.cs b/Parse/Infrastructure/AbsoluteCacheLocationMutator.cs new file mode 100644 index 00000000..416aad1a --- /dev/null +++ b/Parse/Infrastructure/AbsoluteCacheLocationMutator.cs @@ -0,0 +1,34 @@ +using Parse.Abstractions.Infrastructure; + +namespace Parse.Infrastructure +{ + /// + /// An implementation which changes the 's if available. + /// + public class AbsoluteCacheLocationMutator : IServiceHubMutator + { + /// + /// A custom absolute cache file path to be set on the active if it implements . + /// + public string CustomAbsoluteCacheFilePath { get; set; } + + /// + /// + /// + public bool Valid => CustomAbsoluteCacheFilePath is { }; + + /// + /// + /// + /// + /// + public void Mutate(ref IMutableServiceHub target, in IServiceHub composedHub) + { + if ((target as IServiceHub).CacheController is IDiskFileCacheController { } diskFileCacheController) + { + diskFileCacheController.AbsoluteCacheFilePath = CustomAbsoluteCacheFilePath; + diskFileCacheController.RefreshPaths(); + } + } + } +} diff --git a/Parse/Infrastructure/CacheController.cs b/Parse/Infrastructure/CacheController.cs new file mode 100644 index 00000000..f85d2d47 --- /dev/null +++ b/Parse/Infrastructure/CacheController.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure; +using Parse.Infrastructure.Utilities; +using static Parse.Resources; + +namespace Parse.Infrastructure +{ + /// + /// Implements `IStorageController` for PCL targets, based off of PCLStorage. + /// + public class CacheController : IDiskFileCacheController + { + class FileBackedCache : IDataCache + { + public FileBackedCache(FileInfo file) => File = file; + + internal Task SaveAsync() => Lock(() => File.WriteContentAsync(JsonUtilities.Encode(Storage))); + + internal Task LoadAsync() => File.ReadAllTextAsync().ContinueWith(task => + { + lock (Mutex) + { + try + { + Storage = JsonUtilities.Parse(task.Result) as Dictionary; + } + catch + { + Storage = new Dictionary { }; + } + } + }); + + // TODO: Check if the call to ToDictionary is necessary here considering contents is IDictionary. + + internal void Update(IDictionary contents) => Lock(() => Storage = contents.ToDictionary(element => element.Key, element => element.Value)); + + public Task AddAsync(string key, object value) + { + lock (Mutex) + { + Storage[key] = value; + return SaveAsync(); + } + } + + public Task RemoveAsync(string key) + { + lock (Mutex) + { + Storage.Remove(key); + return SaveAsync(); + } + } + + public void Add(string key, object value) => throw new NotSupportedException(FileBackedCacheSynchronousMutationNotSupportedMessage); + + public bool Remove(string key) => throw new NotSupportedException(FileBackedCacheSynchronousMutationNotSupportedMessage); + + public void Add(KeyValuePair item) => throw new NotSupportedException(FileBackedCacheSynchronousMutationNotSupportedMessage); + + public bool Remove(KeyValuePair item) => throw new NotSupportedException(FileBackedCacheSynchronousMutationNotSupportedMessage); + + public bool ContainsKey(string key) => Lock(() => Storage.ContainsKey(key)); + + public bool TryGetValue(string key, out object value) + { + lock (Mutex) + { + return (Result: Storage.TryGetValue(key, out object found), value = found).Result; + } + } + + public void Clear() => Lock(() => Storage.Clear()); + + public bool Contains(KeyValuePair item) => Lock(() => Elements.Contains(item)); + + public void CopyTo(KeyValuePair[] array, int arrayIndex) => Lock(() => Elements.CopyTo(array, arrayIndex)); + + public IEnumerator> GetEnumerator() => Storage.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => Storage.GetEnumerator(); + + public FileInfo File { get; set; } + + public object Mutex { get; set; } = new object { }; + + // ALTNAME: Operate + + TResult Lock(Func operation) + { + lock (Mutex) + { + return operation.Invoke(); + } + } + + void Lock(Action operation) + { + lock (Mutex) + { + operation.Invoke(); + } + } + + ICollection> Elements => Storage as ICollection>; + + Dictionary Storage { get; set; } = new Dictionary { }; + + public ICollection Keys => Storage.Keys; + + public ICollection Values => Storage.Values; + + public int Count => Storage.Count; + + public bool IsReadOnly => Elements.IsReadOnly; + + public object this[string key] + { + get => Storage[key]; + set => throw new NotSupportedException(FileBackedCacheSynchronousMutationNotSupportedMessage); + } + } + + FileInfo File { get; set; } + FileBackedCache Cache { get; set; } + TaskQueue Queue { get; } = new TaskQueue { }; + + /// + /// Creates a Parse storage controller and attempts to extract a previously created settings storage file from the persistent storage location. + /// + public CacheController() { } + + /// + /// Creates a Parse storage controller with the provided wrapper. + /// + /// The file wrapper that the storage controller instance should target + public CacheController(FileInfo file) => EnsureCacheExists(file); + + FileBackedCache EnsureCacheExists(FileInfo file = default) => Cache ??= new FileBackedCache(file ?? (File ??= PersistentCacheFile)); + + /// + /// Loads a settings dictionary from the file wrapped by . + /// + /// A storage dictionary containing the deserialized content of the storage file targeted by the instance + public Task> LoadAsync() + { + // Check if storage dictionary is already created from the controllers file (create if not) + EnsureCacheExists(); + + // Load storage dictionary content async and return the resulting dictionary type + return Queue.Enqueue(toAwait => toAwait.ContinueWith(_ => Cache.LoadAsync().OnSuccess(__ => Cache as IDataCache)).Unwrap(), CancellationToken.None); + } + + /// + /// Saves the requested data. + /// + /// The data to be saved. + /// A data cache containing the saved data. + public Task> SaveAsync(IDictionary contents) => Queue.Enqueue(toAwait => toAwait.ContinueWith(_ => + { + EnsureCacheExists().Update(contents); + return Cache.SaveAsync().OnSuccess(__ => Cache as IDataCache); + }).Unwrap()); + + /// + /// + /// + public void RefreshPaths() => Cache = new FileBackedCache(File = PersistentCacheFile); + + // TODO: Attach the following method to AppDomain.CurrentDomain.ProcessExit if that actually ever made sense for anything except randomly generated file names, otherwise attach the delegate when it is known the file name is a randomly generated string. + + /// + /// Clears the data controlled by this class. + /// + public void Clear() + { + if (new FileInfo(FallbackRelativeCacheFilePath) is { Exists: true } file) + { + file.Delete(); + } + } + + /// + /// + /// + public string RelativeCacheFilePath { get; set; } + + /// + /// + /// + public string AbsoluteCacheFilePath + { + get => StoredAbsoluteCacheFilePath ?? Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), RelativeCacheFilePath ?? FallbackRelativeCacheFilePath)); + set => StoredAbsoluteCacheFilePath = value; + } + + string StoredAbsoluteCacheFilePath { get; set; } + + /// + /// Gets the calculated persistent storage file fallback path for this app execution. + /// + public string FallbackRelativeCacheFilePath => StoredFallbackRelativeCacheFilePath ??= IdentifierBasedRelativeCacheLocationGenerator.Fallback.GetRelativeCacheFilePath(new MutableServiceHub { CacheController = this }); + + string StoredFallbackRelativeCacheFilePath { get; set; } + + /// + /// Gets or creates the file pointed to by and returns it's wrapper as a instance. + /// + public FileInfo PersistentCacheFile + { + get + { + Directory.CreateDirectory(AbsoluteCacheFilePath.Substring(0, AbsoluteCacheFilePath.LastIndexOf(Path.DirectorySeparatorChar))); + + FileInfo file = new FileInfo(AbsoluteCacheFilePath); + if (!file.Exists) + using (file.Create()) + ; // Hopefully the JIT doesn't no-op this. The behaviour of the "using" clause should dictate how the stream is closed, to make sure it happens properly. + + return file; + } + } + + /// + /// Gets the file wrapper for the specified . + /// + /// The relative path to the target file + /// An instance of wrapping the the value + public FileInfo GetRelativeFile(string path) + { + Directory.CreateDirectory((path = Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), path))).Substring(0, path.LastIndexOf(Path.DirectorySeparatorChar))); + return new FileInfo(path); + } + + // MoveAsync + + /// + /// Transfers a file from to . + /// + /// + /// + /// A task that completes once the file move operation form to completes. + public async Task TransferAsync(string originFilePath, string targetFilePath) + { + if (!String.IsNullOrWhiteSpace(originFilePath) && !String.IsNullOrWhiteSpace(targetFilePath) && new FileInfo(originFilePath) is { Exists: true } originFile && new FileInfo(targetFilePath) is { } targetFile) + { + using StreamWriter writer = new StreamWriter(targetFile.OpenWrite(), Encoding.Unicode); + using StreamReader reader = new StreamReader(originFile.OpenRead(), Encoding.Unicode); + + await writer.WriteAsync(await reader.ReadToEndAsync()); + } + } + } +} diff --git a/Parse/Infrastructure/ConcurrentUserServiceHubCloner.cs b/Parse/Infrastructure/ConcurrentUserServiceHubCloner.cs new file mode 100644 index 00000000..44eb69ee --- /dev/null +++ b/Parse/Infrastructure/ConcurrentUserServiceHubCloner.cs @@ -0,0 +1,19 @@ +using System.Linq; +using Parse.Abstractions.Infrastructure; +using Parse.Platform.Users; + +namespace Parse.Infrastructure +{ + public class ConcurrentUserServiceHubCloner : IServiceHubCloner, IServiceHubMutator + { + public bool Valid { get; } = true; + + public IServiceHub BuildHub(in IServiceHub reference, IServiceHubComposer composer, params IServiceHubMutator[] requestedMutators) => composer.BuildHub(default, reference, requestedMutators.Concat(new[] { this }).ToArray()); + + public void Mutate(ref IMutableServiceHub target, in IServiceHub composedHub) + { + target.Cloner = this; + target.CurrentUserController = new ParseCurrentUserController(new TransientCacheController { }, composedHub.ClassController, composedHub.Decoder); + } + } +} diff --git a/Parse/Infrastructure/Control/ParseAddOperation.cs b/Parse/Infrastructure/Control/ParseAddOperation.cs new file mode 100644 index 00000000..b485bbe1 --- /dev/null +++ b/Parse/Infrastructure/Control/ParseAddOperation.cs @@ -0,0 +1,39 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Control; +using Parse.Infrastructure.Data; +using Parse.Infrastructure.Utilities; + +namespace Parse.Infrastructure.Control +{ + public class ParseAddOperation : IParseFieldOperation + { + ReadOnlyCollection Data { get; } + + public ParseAddOperation(IEnumerable objects) => Data = new ReadOnlyCollection(objects.ToList()); + + public object Encode(IServiceHub serviceHub) => new Dictionary + { + ["__op"] = "Add", + ["objects"] = PointerOrLocalIdEncoder.Instance.Encode(Data, serviceHub) + }; + + public IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous) => previous switch + { + null => this, + ParseDeleteOperation { } => new ParseSetOperation(Data.ToList()), + ParseSetOperation { } setOp => new ParseSetOperation(Conversion.To>(setOp.Value).Concat(Data).ToList()), + ParseAddOperation { } addition => new ParseAddOperation(addition.Objects.Concat(Data)), + _ => throw new InvalidOperationException("Operation is invalid after previous operation.") + }; + + public object Apply(object oldValue, string key) => oldValue is { } ? Conversion.To>(oldValue).Concat(Data).ToList() : Data.ToList(); + + public IEnumerable Objects => Data; + } +} diff --git a/Parse/Infrastructure/Control/ParseAddUniqueOperation.cs b/Parse/Infrastructure/Control/ParseAddUniqueOperation.cs new file mode 100644 index 00000000..6d36bb47 --- /dev/null +++ b/Parse/Infrastructure/Control/ParseAddUniqueOperation.cs @@ -0,0 +1,69 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Control; +using Parse.Infrastructure.Data; +using Parse.Infrastructure.Utilities; + +namespace Parse.Infrastructure.Control +{ + public class ParseAddUniqueOperation : IParseFieldOperation + { + ReadOnlyCollection Data { get; } + + public ParseAddUniqueOperation(IEnumerable objects) => Data = new ReadOnlyCollection(objects.Distinct().ToList()); + + public object Encode(IServiceHub serviceHub) => new Dictionary + { + ["__op"] = "AddUnique", + ["objects"] = PointerOrLocalIdEncoder.Instance.Encode(Data, serviceHub) + }; + + public IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous) => previous switch + { + null => this, + ParseDeleteOperation _ => new ParseSetOperation(Data.ToList()), + ParseSetOperation setOp => new ParseSetOperation(Apply(Conversion.To>(setOp.Value), default)), + ParseAddUniqueOperation addition => new ParseAddUniqueOperation(Apply(addition.Objects, default) as IList), + _ => throw new InvalidOperationException("Operation is invalid after previous operation.") + }; + + public object Apply(object oldValue, string key) + { + if (oldValue == null) + { + return Data.ToList(); + } + + List result = Conversion.To>(oldValue).ToList(); + IEqualityComparer comparer = ParseFieldOperations.ParseObjectComparer; + + foreach (object target in Data) + { + if (target is ParseObject) + { + if (!(result.FirstOrDefault(reference => comparer.Equals(target, reference)) is { } matched)) + { + result.Add(target); + } + else + { + result[result.IndexOf(matched)] = target; + } + } + else if (!result.Contains(target, comparer)) + { + result.Add(target); + } + } + + return result; + } + + public IEnumerable Objects => Data; + } +} diff --git a/Parse/Infrastructure/Control/ParseDeleteOperation.cs b/Parse/Infrastructure/Control/ParseDeleteOperation.cs new file mode 100644 index 00000000..6a0feb0a --- /dev/null +++ b/Parse/Infrastructure/Control/ParseDeleteOperation.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System.Collections.Generic; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Control; + +namespace Parse.Infrastructure.Control +{ + /// + /// An operation where a field is deleted from the object. + /// + public class ParseDeleteOperation : IParseFieldOperation + { + internal static object Token { get; } = new object { }; + + public static ParseDeleteOperation Instance { get; } = new ParseDeleteOperation { }; + + private ParseDeleteOperation() { } + + public object Encode(IServiceHub serviceHub) => new Dictionary { ["__op"] = "Delete" }; + + public IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous) => this; + + public object Apply(object oldValue, string key) => Token; + } +} diff --git a/Parse/Internal/Operation/ParseFieldOperations.cs b/Parse/Infrastructure/Control/ParseFieldOperations.cs similarity index 81% rename from Parse/Internal/Operation/ParseFieldOperations.cs rename to Parse/Infrastructure/Control/ParseFieldOperations.cs index e5638978..593a02a3 100644 --- a/Parse/Internal/Operation/ParseFieldOperations.cs +++ b/Parse/Infrastructure/Control/ParseFieldOperations.cs @@ -2,15 +2,16 @@ using System; using System.Collections.Generic; +using Parse.Abstractions.Infrastructure.Control; -namespace Parse.Core.Internal +namespace Parse.Infrastructure.Control { public class ParseObjectIdComparer : IEqualityComparer { bool IEqualityComparer.Equals(object p1, object p2) { - var parseObj1 = p1 as ParseObject; - var parseObj2 = p2 as ParseObject; + ParseObject parseObj1 = p1 as ParseObject; + ParseObject parseObj2 = p2 as ParseObject; if (parseObj1 != null && parseObj2 != null) { return Equals(parseObj1.ObjectId, parseObj2.ObjectId); @@ -20,7 +21,7 @@ bool IEqualityComparer.Equals(object p1, object p2) public int GetHashCode(object p) { - var parseObject = p as ParseObject; + ParseObject parseObject = p as ParseObject; if (parseObject != null) { return parseObject.ObjectId.GetHashCode(); @@ -33,10 +34,7 @@ static class ParseFieldOperations { private static ParseObjectIdComparer comparer; - public static IParseFieldOperation Decode(IDictionary json) - { - throw new NotImplementedException(); - } + public static IParseFieldOperation Decode(IDictionary json) => throw new NotImplementedException(); public static IEqualityComparer ParseObjectComparer { diff --git a/Parse/Infrastructure/Control/ParseIncrementOperation.cs b/Parse/Infrastructure/Control/ParseIncrementOperation.cs new file mode 100644 index 00000000..eec00787 --- /dev/null +++ b/Parse/Infrastructure/Control/ParseIncrementOperation.cs @@ -0,0 +1,126 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections.Generic; +using System.Linq; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Control; + +namespace Parse.Infrastructure.Control +{ + public class ParseIncrementOperation : IParseFieldOperation + { + // Defines adders for all of the implicit conversions: http://msdn.microsoft.com/en-US/library/y5b434w4(v=vs.80).aspx. + + static IDictionary, Func> Adders { get; } = new Dictionary, Func> + { + [new Tuple(typeof(sbyte), typeof(sbyte))] = (left, right) => (sbyte) left + (sbyte) right, + [new Tuple(typeof(sbyte), typeof(short))] = (left, right) => (sbyte) left + (short) right, + [new Tuple(typeof(sbyte), typeof(int))] = (left, right) => (sbyte) left + (int) right, + [new Tuple(typeof(sbyte), typeof(long))] = (left, right) => (sbyte) left + (long) right, + [new Tuple(typeof(sbyte), typeof(float))] = (left, right) => (sbyte) left + (float) right, + [new Tuple(typeof(sbyte), typeof(double))] = (left, right) => (sbyte) left + (double) right, + [new Tuple(typeof(sbyte), typeof(decimal))] = (left, right) => (sbyte) left + (decimal) right, + [new Tuple(typeof(byte), typeof(byte))] = (left, right) => (byte) left + (byte) right, + [new Tuple(typeof(byte), typeof(short))] = (left, right) => (byte) left + (short) right, + [new Tuple(typeof(byte), typeof(ushort))] = (left, right) => (byte) left + (ushort) right, + [new Tuple(typeof(byte), typeof(int))] = (left, right) => (byte) left + (int) right, + [new Tuple(typeof(byte), typeof(uint))] = (left, right) => (byte) left + (uint) right, + [new Tuple(typeof(byte), typeof(long))] = (left, right) => (byte) left + (long) right, + [new Tuple(typeof(byte), typeof(ulong))] = (left, right) => (byte) left + (ulong) right, + [new Tuple(typeof(byte), typeof(float))] = (left, right) => (byte) left + (float) right, + [new Tuple(typeof(byte), typeof(double))] = (left, right) => (byte) left + (double) right, + [new Tuple(typeof(byte), typeof(decimal))] = (left, right) => (byte) left + (decimal) right, + [new Tuple(typeof(short), typeof(short))] = (left, right) => (short) left + (short) right, + [new Tuple(typeof(short), typeof(int))] = (left, right) => (short) left + (int) right, + [new Tuple(typeof(short), typeof(long))] = (left, right) => (short) left + (long) right, + [new Tuple(typeof(short), typeof(float))] = (left, right) => (short) left + (float) right, + [new Tuple(typeof(short), typeof(double))] = (left, right) => (short) left + (double) right, + [new Tuple(typeof(short), typeof(decimal))] = (left, right) => (short) left + (decimal) right, + [new Tuple(typeof(ushort), typeof(ushort))] = (left, right) => (ushort) left + (ushort) right, + [new Tuple(typeof(ushort), typeof(int))] = (left, right) => (ushort) left + (int) right, + [new Tuple(typeof(ushort), typeof(uint))] = (left, right) => (ushort) left + (uint) right, + [new Tuple(typeof(ushort), typeof(long))] = (left, right) => (ushort) left + (long) right, + [new Tuple(typeof(ushort), typeof(ulong))] = (left, right) => (ushort) left + (ulong) right, + [new Tuple(typeof(ushort), typeof(float))] = (left, right) => (ushort) left + (float) right, + [new Tuple(typeof(ushort), typeof(double))] = (left, right) => (ushort) left + (double) right, + [new Tuple(typeof(ushort), typeof(decimal))] = (left, right) => (ushort) left + (decimal) right, + [new Tuple(typeof(int), typeof(int))] = (left, right) => (int) left + (int) right, + [new Tuple(typeof(int), typeof(long))] = (left, right) => (int) left + (long) right, + [new Tuple(typeof(int), typeof(float))] = (left, right) => (int) left + (float) right, + [new Tuple(typeof(int), typeof(double))] = (left, right) => (int) left + (double) right, + [new Tuple(typeof(int), typeof(decimal))] = (left, right) => (int) left + (decimal) right, + [new Tuple(typeof(uint), typeof(uint))] = (left, right) => (uint) left + (uint) right, + [new Tuple(typeof(uint), typeof(long))] = (left, right) => (uint) left + (long) right, + [new Tuple(typeof(uint), typeof(ulong))] = (left, right) => (uint) left + (ulong) right, + [new Tuple(typeof(uint), typeof(float))] = (left, right) => (uint) left + (float) right, + [new Tuple(typeof(uint), typeof(double))] = (left, right) => (uint) left + (double) right, + [new Tuple(typeof(uint), typeof(decimal))] = (left, right) => (uint) left + (decimal) right, + [new Tuple(typeof(long), typeof(long))] = (left, right) => (long) left + (long) right, + [new Tuple(typeof(long), typeof(float))] = (left, right) => (long) left + (float) right, + [new Tuple(typeof(long), typeof(double))] = (left, right) => (long) left + (double) right, + [new Tuple(typeof(long), typeof(decimal))] = (left, right) => (long) left + (decimal) right, + [new Tuple(typeof(char), typeof(char))] = (left, right) => (char) left + (char) right, + [new Tuple(typeof(char), typeof(ushort))] = (left, right) => (char) left + (ushort) right, + [new Tuple(typeof(char), typeof(int))] = (left, right) => (char) left + (int) right, + [new Tuple(typeof(char), typeof(uint))] = (left, right) => (char) left + (uint) right, + [new Tuple(typeof(char), typeof(long))] = (left, right) => (char) left + (long) right, + [new Tuple(typeof(char), typeof(ulong))] = (left, right) => (char) left + (ulong) right, + [new Tuple(typeof(char), typeof(float))] = (left, right) => (char) left + (float) right, + [new Tuple(typeof(char), typeof(double))] = (left, right) => (char) left + (double) right, + [new Tuple(typeof(char), typeof(decimal))] = (left, right) => (char) left + (decimal) right, + [new Tuple(typeof(float), typeof(float))] = (left, right) => (float) left + (float) right, + [new Tuple(typeof(float), typeof(double))] = (left, right) => (float) left + (double) right, + [new Tuple(typeof(ulong), typeof(ulong))] = (left, right) => (ulong) left + (ulong) right, + [new Tuple(typeof(ulong), typeof(float))] = (left, right) => (ulong) left + (float) right, + [new Tuple(typeof(ulong), typeof(double))] = (left, right) => (ulong) left + (double) right, + [new Tuple(typeof(ulong), typeof(decimal))] = (left, right) => (ulong) left + (decimal) right, + [new Tuple(typeof(double), typeof(double))] = (left, right) => (double) left + (double) right, + [new Tuple(typeof(decimal), typeof(decimal))] = (left, right) => (decimal) left + (decimal) right + }; + + static ParseIncrementOperation() + { + // Generate the adders in the other direction + + foreach (Tuple pair in Adders.Keys.ToList()) + { + if (pair.Item1.Equals(pair.Item2)) + { + continue; + } + + Tuple reversePair = new Tuple(pair.Item2, pair.Item1); + Func func = Adders[pair]; + Adders[reversePair] = (left, right) => func(right, left); + } + } + + public ParseIncrementOperation(object amount) => Amount = amount; + + public object Encode(IServiceHub serviceHub) => new Dictionary + { + ["__op"] = "Increment", + ["amount"] = Amount + }; + + static object Add(object first, object second) => Adders.TryGetValue(new Tuple(first.GetType(), second.GetType()), out Func adder) ? adder(first, second) : throw new InvalidCastException($"Could not add objects of type {first.GetType()} and {second.GetType()} to each other."); + + public IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous) => previous switch + { + null => this, + ParseDeleteOperation _ => new ParseSetOperation(Amount), + + // This may be a bug, but it was in the original logic. + + ParseSetOperation { Value: string { } } => throw new InvalidOperationException("Cannot increment a non-number type."), + ParseSetOperation { Value: var value } => new ParseSetOperation(Add(value, Amount)), + ParseIncrementOperation { Amount: var amount } => new ParseIncrementOperation(Add(amount, Amount)), + _ => throw new InvalidOperationException("Operation is invalid after previous operation.") + }; + + public object Apply(object oldValue, string key) => oldValue is string ? throw new InvalidOperationException("Cannot increment a non-number type.") : Add(oldValue ?? 0, Amount); + + public object Amount { get; } + } +} diff --git a/Parse/Infrastructure/Control/ParseRelationOperation.cs b/Parse/Infrastructure/Control/ParseRelationOperation.cs new file mode 100644 index 00000000..43915d77 --- /dev/null +++ b/Parse/Infrastructure/Control/ParseRelationOperation.cs @@ -0,0 +1,106 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Control; +using Parse.Abstractions.Platform.Objects; +using Parse.Infrastructure.Data; + +namespace Parse.Infrastructure.Control +{ + public class ParseRelationOperation : IParseFieldOperation + { + IList Additions { get; } + + IList Removals { get; } + + IParseObjectClassController ClassController { get; } + + ParseRelationOperation(IParseObjectClassController classController) => ClassController = classController; + + ParseRelationOperation(IParseObjectClassController classController, IEnumerable adds, IEnumerable removes, string targetClassName) : this(classController) + { + TargetClassName = targetClassName; + Additions = new ReadOnlyCollection(adds.ToList()); + Removals = new ReadOnlyCollection(removes.ToList()); + } + + public ParseRelationOperation(IParseObjectClassController classController, IEnumerable adds, IEnumerable removes) : this(classController) + { + adds ??= new ParseObject[0]; + removes ??= new ParseObject[0]; + + TargetClassName = adds.Concat(removes).Select(entity => entity.ClassName).FirstOrDefault(); + Additions = new ReadOnlyCollection(GetIdsFromObjects(adds).ToList()); + Removals = new ReadOnlyCollection(GetIdsFromObjects(removes).ToList()); + } + + public object Encode(IServiceHub serviceHub) + { + List additions = Additions.Select(id => PointerOrLocalIdEncoder.Instance.Encode(ClassController.CreateObjectWithoutData(TargetClassName, id, serviceHub), serviceHub)).ToList(), removals = Removals.Select(id => PointerOrLocalIdEncoder.Instance.Encode(ClassController.CreateObjectWithoutData(TargetClassName, id, serviceHub), serviceHub)).ToList(); + + Dictionary addition = additions.Count == 0 ? default : new Dictionary + { + ["__op"] = "AddRelation", + ["objects"] = additions + }; + + Dictionary removal = removals.Count == 0 ? default : new Dictionary + { + ["__op"] = "RemoveRelation", + ["objects"] = removals + }; + + if (addition is { } && removal is { }) + { + return new Dictionary + { + ["__op"] = "Batch", + ["ops"] = new[] { addition, removal } + }; + } + return addition ?? removal; + } + + public IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous) => previous switch + { + null => this, + ParseDeleteOperation { } => throw new InvalidOperationException("You can't modify a relation after deleting it."), + ParseRelationOperation { } other when other.TargetClassName != TargetClassName => throw new InvalidOperationException($"Related object must be of class {other.TargetClassName}, but {TargetClassName} was passed in."), + ParseRelationOperation { ClassController: var classController } other => new ParseRelationOperation(classController, Additions.Union(other.Additions.Except(Removals)).ToList(), Removals.Union(other.Removals.Except(Additions)).ToList(), TargetClassName), + _ => throw new InvalidOperationException("Operation is invalid after previous operation.") + }; + + public object Apply(object oldValue, string key) => oldValue switch + { + _ when Additions.Count == 0 && Removals.Count == 0 => default, + null => ClassController.CreateRelation(null, key, TargetClassName), + ParseRelationBase { TargetClassName: { } oldClassname } when oldClassname != TargetClassName => throw new InvalidOperationException($"Related object must be a {oldClassname}, but a {TargetClassName} was passed in."), + ParseRelationBase { } oldRelation => (Relation: oldRelation, oldRelation.TargetClassName = TargetClassName).Relation, + _ => throw new InvalidOperationException("Operation is invalid after previous operation.") + }; + + public string TargetClassName { get; } + + IEnumerable GetIdsFromObjects(IEnumerable objects) + { + foreach (ParseObject entity in objects) + { + if (entity.ObjectId is null) + { + throw new ArgumentException("You can't add an unsaved ParseObject to a relation."); + } + + if (entity.ClassName != TargetClassName) + { + throw new ArgumentException($"Tried to create a ParseRelation with 2 different types: {TargetClassName} and {entity.ClassName}"); + } + } + + return objects.Select(entity => entity.ObjectId).Distinct(); + } + } +} diff --git a/Parse/Infrastructure/Control/ParseRemoveOperation.cs b/Parse/Infrastructure/Control/ParseRemoveOperation.cs new file mode 100644 index 00000000..17987e0c --- /dev/null +++ b/Parse/Infrastructure/Control/ParseRemoveOperation.cs @@ -0,0 +1,39 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Control; +using Parse.Infrastructure.Data; +using Parse.Infrastructure.Utilities; + +namespace Parse.Infrastructure.Control +{ + public class ParseRemoveOperation : IParseFieldOperation + { + ReadOnlyCollection Data { get; } + + public ParseRemoveOperation(IEnumerable objects) => Data = new ReadOnlyCollection(objects.Distinct().ToList()); + + public object Encode(IServiceHub serviceHub) => new Dictionary + { + ["__op"] = "Remove", + ["objects"] = PointerOrLocalIdEncoder.Instance.Encode(Data, serviceHub) + }; + + public IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous) => previous switch + { + null => this, + ParseDeleteOperation _ => previous, + ParseSetOperation setOp => new ParseSetOperation(Apply(Conversion.As>(setOp.Value), default)), + ParseRemoveOperation oldOp => new ParseRemoveOperation(oldOp.Objects.Concat(Data)), + _ => throw new InvalidOperationException("Operation is invalid after previous operation.") + }; + + public object Apply(object oldValue, string key) => oldValue is { } ? Conversion.As>(oldValue).Except(Data, ParseFieldOperations.ParseObjectComparer).ToList() : new List { }; + + public IEnumerable Objects => Data; + } +} diff --git a/Parse/Internal/Operation/ParseSetOperation.cs b/Parse/Infrastructure/Control/ParseSetOperation.cs similarity index 52% rename from Parse/Internal/Operation/ParseSetOperation.cs rename to Parse/Infrastructure/Control/ParseSetOperation.cs index ebefc190..30c27a97 100644 --- a/Parse/Internal/Operation/ParseSetOperation.cs +++ b/Parse/Infrastructure/Control/ParseSetOperation.cs @@ -1,28 +1,20 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. -namespace Parse.Core.Internal +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Control; +using Parse.Infrastructure.Data; + +namespace Parse.Infrastructure.Control { public class ParseSetOperation : IParseFieldOperation { - public ParseSetOperation(object value) - { - Value = value; - } + public ParseSetOperation(object value) => Value = value; - public object Encode() - { - return PointerOrLocalIdEncoder.Instance.Encode(Value); - } + public object Encode(IServiceHub serviceHub) => PointerOrLocalIdEncoder.Instance.Encode(Value, serviceHub); - public IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous) - { - return this; - } + public IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous) => this; - public object Apply(object oldValue, string key) - { - return Value; - } + public object Apply(object oldValue, string key) => Value; public object Value { get; private set; } } diff --git a/Parse/Infrastructure/Data/NoObjectsEncoder.cs b/Parse/Infrastructure/Data/NoObjectsEncoder.cs new file mode 100644 index 00000000..30215c3a --- /dev/null +++ b/Parse/Infrastructure/Data/NoObjectsEncoder.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections.Generic; + +namespace Parse.Infrastructure.Data +{ + // This class isn't really a Singleton, but since it has no state, it's more efficient to get the default instance. + + /// + /// A that throws an exception if it attempts to encode + /// a + /// + public class NoObjectsEncoder : ParseDataEncoder + { + public static NoObjectsEncoder Instance { get; } = new NoObjectsEncoder(); + + protected override IDictionary EncodeObject(ParseObject value) => throw new ArgumentException("ParseObjects not allowed here."); + } +} diff --git a/Parse/Infrastructure/Data/ParseDataDecoder.cs b/Parse/Infrastructure/Data/ParseDataDecoder.cs new file mode 100644 index 00000000..7be73093 --- /dev/null +++ b/Parse/Infrastructure/Data/ParseDataDecoder.cs @@ -0,0 +1,50 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Data; +using Parse.Abstractions.Platform.Objects; +using Parse.Infrastructure.Control; +using Parse.Infrastructure.Utilities; + +namespace Parse.Infrastructure.Data +{ + public class ParseDataDecoder : IParseDataDecoder + { + // Prevent default constructor. + + IParseObjectClassController ClassController { get; } + + public ParseDataDecoder(IParseObjectClassController classController) => ClassController = classController; + + static string[] Types { get; } = { "Date", "Bytes", "Pointer", "File", "GeoPoint", "Object", "Relation" }; + + public object Decode(object data, IServiceHub serviceHub) => data switch + { + null => default, + IDictionary { } dictionary when dictionary.ContainsKey("__op") => ParseFieldOperations.Decode(dictionary), + IDictionary { } dictionary when dictionary.TryGetValue("__type", out object type) && Types.Contains(type) => type switch + { + "Date" => ParseDate(dictionary["iso"] as string), + "Bytes" => Convert.FromBase64String(dictionary["base64"] as string), + "Pointer" => DecodePointer(dictionary["className"] as string, dictionary["objectId"] as string, serviceHub), + "File" => new ParseFile(dictionary["name"] as string, new Uri(dictionary["url"] as string)), + "GeoPoint" => new ParseGeoPoint(Conversion.To(dictionary["latitude"]), Conversion.To(dictionary["longitude"])), + "Object" => ClassController.GenerateObjectFromState(ParseObjectCoder.Instance.Decode(dictionary, this, serviceHub), dictionary["className"] as string, serviceHub), + "Relation" => serviceHub.CreateRelation(null, null, dictionary["className"] as string) + }, + IDictionary { } dictionary => dictionary.ToDictionary(pair => pair.Key, pair => Decode(pair.Value, serviceHub)), + IList { } list => list.Select(item => Decode(item, serviceHub)).ToList(), + _ => data + }; + + protected virtual object DecodePointer(string className, string objectId, IServiceHub serviceHub) => ClassController.CreateObjectWithoutData(className, objectId, serviceHub); + + // TODO(hallucinogen): Figure out if we should be more flexible with the date formats we accept. + + public static DateTime ParseDate(string input) => DateTime.ParseExact(input, ParseClient.DateFormatStrings, CultureInfo.InvariantCulture, DateTimeStyles.None); + } +} diff --git a/Parse/Infrastructure/Data/ParseDataEncoder.cs b/Parse/Infrastructure/Data/ParseDataEncoder.cs new file mode 100644 index 00000000..8cee8060 --- /dev/null +++ b/Parse/Infrastructure/Data/ParseDataEncoder.cs @@ -0,0 +1,73 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Control; +using Parse.Infrastructure.Utilities; + +namespace Parse.Infrastructure.Data +{ + /// + /// A ParseEncoder can be used to transform objects such as into JSON + /// data structures. + /// + /// + public abstract class ParseDataEncoder + { + public static bool Validate(object value) => value is null || value.GetType().IsPrimitive || value is string || value is ParseObject || value is ParseACL || value is ParseFile || value is ParseGeoPoint || value is ParseRelationBase || value is DateTime || value is byte[] || Conversion.As>(value) is { } || Conversion.As>(value) is { }; + + // If this object has a special encoding, encode it and return the encoded object. Otherwise, just return the original object. + + public object Encode(object value, IServiceHub serviceHub) => value switch + { + DateTime { } date => new Dictionary + { + ["iso"] = date.ToString(ParseClient.DateFormatStrings.First(), CultureInfo.InvariantCulture), + ["__type"] = "Date" + }, + byte[] { } bytes => new Dictionary + { + ["__type"] = "Bytes", + ["base64"] = Convert.ToBase64String(bytes) + }, + ParseObject { } entity => EncodeObject(entity), + IJsonConvertible { } jsonConvertible => jsonConvertible.ConvertToJSON(), + { } when Conversion.As>(value) is { } dictionary => dictionary.ToDictionary(pair => pair.Key, pair => Encode(pair.Value, serviceHub)), + { } when Conversion.As>(value) is { } list => EncodeList(list, serviceHub), + + // TODO (hallucinogen): convert IParseFieldOperation to IJsonConvertible + + IParseFieldOperation { } fieldOperation => fieldOperation.Encode(serviceHub), + _ => value + }; + + protected abstract IDictionary EncodeObject(ParseObject value); + + object EncodeList(IList list, IServiceHub serviceHub) + { + List encoded = new List { }; + + // We need to explicitly cast `list` to `List` rather than `IList` because IL2CPP is stricter than the usual Unity AOT compiler pipeline. + + if (ParseClient.IL2CPPCompiled && list.GetType().IsArray) + { + list = new List(list); + } + + foreach (object item in list) + { + if (!Validate(item)) + { + throw new ArgumentException("Invalid type for value in an array"); + } + + encoded.Add(Encode(item, serviceHub)); + } + + return encoded; + } + } +} diff --git a/Parse/Internal/Encoding/ParseObjectCoder.cs b/Parse/Infrastructure/Data/ParseObjectCoder.cs similarity index 61% rename from Parse/Internal/Encoding/ParseObjectCoder.cs rename to Parse/Infrastructure/Data/ParseObjectCoder.cs index 12c6b208..8dedd347 100644 --- a/Parse/Internal/Encoding/ParseObjectCoder.cs +++ b/Parse/Infrastructure/Data/ParseObjectCoder.cs @@ -2,42 +2,48 @@ using System; using System.Collections.Generic; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Control; +using Parse.Abstractions.Infrastructure.Data; +using Parse.Abstractions.Platform.Objects; +using Parse.Platform.Objects; -namespace Parse.Core.Internal +namespace Parse.Infrastructure.Data { // TODO: (richardross) refactor entire parse coder interfaces. + public class ParseObjectCoder { - public static ParseObjectCoder Instance { get; } = new ParseObjectCoder(); + public static ParseObjectCoder Instance { get; } = new ParseObjectCoder { }; // Prevent default constructor. - private ParseObjectCoder() { } - public IDictionary Encode(T state, IDictionary operations, ParseEncoder encoder) where T : IObjectState + ParseObjectCoder() { } + + public IDictionary Encode(T state, IDictionary operations, ParseDataEncoder encoder, IServiceHub serviceHub) where T : IObjectState { - Dictionary result = new Dictionary(); + Dictionary result = new Dictionary { }; foreach (KeyValuePair pair in operations) { // Serialize the data IParseFieldOperation operation = pair.Value; - result[pair.Key] = encoder.Encode(operation); + result[pair.Key] = encoder.Encode(operation, serviceHub); } return result; } - public IObjectState Decode(IDictionary data, ParseDecoder decoder) + public IObjectState Decode(IDictionary data, IParseDataDecoder decoder, IServiceHub serviceHub) { - IDictionary serverData = new Dictionary(); - Dictionary mutableData = new Dictionary(data); - string objectId = extractFromDictionary(mutableData, "objectId", (obj) => obj as string); - DateTime? createdAt = extractFromDictionary(mutableData, "createdAt", (obj) => ParseDecoder.ParseDate(obj as string)); - DateTime? updatedAt = extractFromDictionary(mutableData, "updatedAt", (obj) => ParseDecoder.ParseDate(obj as string)); + IDictionary serverData = new Dictionary { }, mutableData = new Dictionary(data); + + string objectId = Extract(mutableData, "objectId", (obj) => obj as string); + DateTime? createdAt = Extract(mutableData, "createdAt", (obj) => ParseDataDecoder.ParseDate(obj as string)), updatedAt = Extract(mutableData, "updatedAt", (obj) => ParseDataDecoder.ParseDate(obj as string)); if (mutableData.ContainsKey("ACL")) { - serverData["ACL"] = extractFromDictionary(mutableData, "ACL", (obj) => new ParseACL(obj as IDictionary)); + serverData["ACL"] = Extract(mutableData, "ACL", (obj) => new ParseACL(obj as IDictionary)); } if (createdAt != null && updatedAt == null) @@ -46,6 +52,7 @@ public IObjectState Decode(IDictionary data, ParseDecoder decode } // Bring in the new server data. + foreach (KeyValuePair pair in mutableData) { if (pair.Key == "__type" || pair.Key == "className") @@ -53,8 +60,7 @@ public IObjectState Decode(IDictionary data, ParseDecoder decode continue; } - object value = pair.Value; - serverData[pair.Key] = decoder.Decode(value); + serverData[pair.Key] = decoder.Decode(pair.Value, serviceHub); } return new MutableObjectState @@ -66,9 +72,10 @@ public IObjectState Decode(IDictionary data, ParseDecoder decode }; } - private T extractFromDictionary(IDictionary data, string key, Func action) + T Extract(IDictionary data, string key, Func action) { T result = default; + if (data.ContainsKey(key)) { result = action(data[key]); diff --git a/Parse/Infrastructure/Data/PointerOrLocalIdEncoder.cs b/Parse/Infrastructure/Data/PointerOrLocalIdEncoder.cs new file mode 100644 index 00000000..d6a81d1d --- /dev/null +++ b/Parse/Infrastructure/Data/PointerOrLocalIdEncoder.cs @@ -0,0 +1,32 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections.Generic; + +namespace Parse.Infrastructure.Data +{ + /// + /// A that encodes as pointers. If the object does not have an , uses a local id. + /// + public class PointerOrLocalIdEncoder : ParseDataEncoder + { + public static PointerOrLocalIdEncoder Instance { get; } = new PointerOrLocalIdEncoder { }; + + protected override IDictionary EncodeObject(ParseObject value) + { + if (value.ObjectId is null) + { + // TODO (hallucinogen): handle local id. For now we throw. + + throw new InvalidOperationException("Cannot create a pointer to an object without an objectId."); + } + + return new Dictionary + { + ["__type"] = "Pointer", + ["className"] = value.ClassName, + ["objectId"] = value.ObjectId + }; + } + } +} diff --git a/Parse/Public/ParseUploadProgressEventArgs.cs b/Parse/Infrastructure/DataTransferLevel.cs similarity index 70% rename from Parse/Public/ParseUploadProgressEventArgs.cs rename to Parse/Infrastructure/DataTransferLevel.cs index 35df390c..6dfcf1dd 100644 --- a/Parse/Public/ParseUploadProgressEventArgs.cs +++ b/Parse/Infrastructure/DataTransferLevel.cs @@ -1,19 +1,18 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. using System; +using Parse.Abstractions.Infrastructure; -namespace Parse +namespace Parse.Infrastructure { /// /// Represents upload progress. /// - public class ParseUploadProgressEventArgs : EventArgs + public class DataTransferLevel : EventArgs, IDataTransferLevel { - public ParseUploadProgressEventArgs() { } - /// - /// Gets the progress (a number between 0.0 and 1.0) of an upload. + /// Gets the progress (a number between 0.0 and 1.0) of an upload or download. /// - public double Progress { get; set; } + public double Amount { get; set; } } } diff --git a/Parse/Infrastructure/EnvironmentData.cs b/Parse/Infrastructure/EnvironmentData.cs new file mode 100644 index 00000000..3ec2061d --- /dev/null +++ b/Parse/Infrastructure/EnvironmentData.cs @@ -0,0 +1,37 @@ +using System; +using System.Runtime.InteropServices; +using Parse.Abstractions.Infrastructure; + +namespace Parse.Infrastructure +{ + /// + /// Inferred data about the environment in which parse is operating. + /// + public class EnvironmentData : IEnvironmentData + { + /// + /// A instance that the Parse SDK will attempt to generate from environment metadata it should be able to access. + /// + public static EnvironmentData Inferred => new EnvironmentData + { + TimeZone = TimeZoneInfo.Local.StandardName, + OSVersion = RuntimeInformation.OSDescription ?? Environment.OSVersion.ToString(), + Platform = RuntimeInformation.FrameworkDescription ?? ".NET" + }; + + /// + /// The active time zone for the app and/or system. + /// + public string TimeZone { get; set; } + + /// + /// The host operating system version of the platform the host application is operating in. + /// + public string OSVersion { get; set; } + + /// + /// The target platform the app is running on. Defaults to .NET. + /// + public string Platform { get; set; } + } +} diff --git a/Parse/Infrastructure/Execution/ParseCommand.cs b/Parse/Infrastructure/Execution/ParseCommand.cs new file mode 100644 index 00000000..705b2e1e --- /dev/null +++ b/Parse/Infrastructure/Execution/ParseCommand.cs @@ -0,0 +1,56 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Parse.Infrastructure.Utilities; + +namespace Parse.Infrastructure.Execution +{ + /// + /// ParseCommand is an with pre-populated + /// headers. + /// + public class ParseCommand : WebRequest + { + public IDictionary DataObject { get; private set; } + + public override Stream Data + { + get => base.Data ??= DataObject is { } ? new MemoryStream(Encoding.UTF8.GetBytes(JsonUtilities.Encode(DataObject))) : default; + set => base.Data = value; + } + + public ParseCommand(string relativeUri, string method, string sessionToken = null, IList> headers = null, IDictionary data = null) : this(relativeUri: relativeUri, method: method, sessionToken: sessionToken, headers: headers, stream: null, contentType: data != null ? "application/json" : null) => DataObject = data; + + public ParseCommand(string relativeUri, string method, string sessionToken = null, IList> headers = null, Stream stream = null, string contentType = null) + { + Path = relativeUri; + Method = method; + Data = stream; + Headers = new List>(headers ?? Enumerable.Empty>()); + + if (!String.IsNullOrEmpty(sessionToken)) + { + Headers.Add(new KeyValuePair("X-Parse-Session-Token", sessionToken)); + } + + if (!String.IsNullOrEmpty(contentType)) + { + Headers.Add(new KeyValuePair("Content-Type", contentType)); + } + } + + public ParseCommand(ParseCommand other) + { + Resource = other.Resource; + Path = other.Path; + Method = other.Method; + DataObject = other.DataObject; + Headers = new List>(other.Headers); + Data = other.Data; + } + } +} diff --git a/Parse/Infrastructure/Execution/ParseCommandRunner.cs b/Parse/Infrastructure/Execution/ParseCommandRunner.cs new file mode 100644 index 00000000..ab74abb4 --- /dev/null +++ b/Parse/Infrastructure/Execution/ParseCommandRunner.cs @@ -0,0 +1,160 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Execution; +using Parse.Abstractions.Platform.Installations; +using Parse.Abstractions.Platform.Users; +using Parse.Infrastructure.Utilities; + +namespace Parse.Infrastructure.Execution +{ + /// + /// The command runner for all SDK operations that need to interact with the targeted deployment of Parse Server. + /// + public class ParseCommandRunner : IParseCommandRunner + { + IWebClient WebClient { get; } + + IParseInstallationController InstallationController { get; } + + IMetadataController MetadataController { get; } + + IServerConnectionData ServerConnectionData { get; } + + Lazy UserController { get; } + + IWebClient GetWebClient() => WebClient; + + /// + /// Creates a new Parse SDK command runner. + /// + /// The implementation instance to use. + /// The implementation instance to use. + public ParseCommandRunner(IWebClient webClient, IParseInstallationController installationController, IMetadataController metadataController, IServerConnectionData serverConnectionData, Lazy userController) + { + WebClient = webClient; + InstallationController = installationController; + MetadataController = metadataController; + ServerConnectionData = serverConnectionData; + UserController = userController; + } + + /// + /// Runs a specified . + /// + /// The to run. + /// An instance to push upload progress data to. + /// An instance to push download progress data to. + /// An asynchronous operation cancellation token that dictates if and when the operation should be cancelled. + /// + public Task>> RunCommandAsync(ParseCommand command, IProgress uploadProgress = null, IProgress downloadProgress = null, CancellationToken cancellationToken = default) => PrepareCommand(command).ContinueWith(commandTask => GetWebClient().ExecuteAsync(commandTask.Result, uploadProgress, downloadProgress, cancellationToken).OnSuccess(task => + { + cancellationToken.ThrowIfCancellationRequested(); + + Tuple response = task.Result; + string content = response.Item2; + int responseCode = (int) response.Item1; + + if (responseCode >= 500) + { + // Server error, return InternalServerError. + + throw new ParseFailureException(ParseFailureException.ErrorCode.InternalServerError, response.Item2); + } + else if (content is { }) + { + IDictionary contentJson = default; + + try + { + // TODO: Newer versions of Parse Server send the failure results back as HTML. + + contentJson = content.StartsWith("[") ? new Dictionary { ["results"] = JsonUtilities.Parse(content) } : JsonUtilities.Parse(content) as IDictionary; + } + catch (Exception e) + { + throw new ParseFailureException(ParseFailureException.ErrorCode.OtherCause, "Invalid or alternatively-formatted response recieved from server.", e); + } + + if (responseCode < 200 || responseCode > 299) + { + throw new ParseFailureException(contentJson.ContainsKey("code") ? (ParseFailureException.ErrorCode) (long) contentJson["code"] : ParseFailureException.ErrorCode.OtherCause, contentJson.ContainsKey("error") ? contentJson["error"] as string : content); + } + + return new Tuple>(response.Item1, contentJson); + } + return new Tuple>(response.Item1, null); + })).Unwrap(); + + Task PrepareCommand(ParseCommand command) + { + ParseCommand newCommand = new ParseCommand(command) + { + Resource = ServerConnectionData.ServerURI + }; + + Task installationIdFetchTask = InstallationController.GetAsync().ContinueWith(task => + { + lock (newCommand.Headers) + { + newCommand.Headers.Add(new KeyValuePair("X-Parse-Installation-Id", task.Result.ToString())); + } + + return newCommand; + }); + + // Locks needed due to installationFetchTask continuation newCommand.Headers.Add-call-related race condition (occurred once in Unity). + // TODO: Consider removal of installationFetchTask variable. + + lock (newCommand.Headers) + { + newCommand.Headers.Add(new KeyValuePair("X-Parse-Application-Id", ServerConnectionData.ApplicationID)); + newCommand.Headers.Add(new KeyValuePair("X-Parse-Client-Version", ParseClient.Version.ToString())); + + if (ServerConnectionData.Headers != null) + { + foreach (KeyValuePair header in ServerConnectionData.Headers) + { + newCommand.Headers.Add(header); + } + } + + if (!String.IsNullOrEmpty(MetadataController.HostManifestData.Version)) + { + newCommand.Headers.Add(new KeyValuePair("X-Parse-App-Build-Version", MetadataController.HostManifestData.Version)); + } + + if (!String.IsNullOrEmpty(MetadataController.HostManifestData.ShortVersion)) + { + newCommand.Headers.Add(new KeyValuePair("X-Parse-App-Display-Version", MetadataController.HostManifestData.ShortVersion)); + } + + if (!String.IsNullOrEmpty(MetadataController.EnvironmentData.OSVersion)) + { + newCommand.Headers.Add(new KeyValuePair("X-Parse-OS-Version", MetadataController.EnvironmentData.OSVersion)); + } + + if (!String.IsNullOrEmpty(ServerConnectionData.MasterKey)) + { + newCommand.Headers.Add(new KeyValuePair("X-Parse-Master-Key", ServerConnectionData.MasterKey)); + } + else + { + newCommand.Headers.Add(new KeyValuePair("X-Parse-Windows-Key", ServerConnectionData.Key)); + } + + if (UserController.Value.RevocableSessionEnabled) + { + newCommand.Headers.Add(new KeyValuePair("X-Parse-Revocable-Session", "1")); + } + } + + return installationIdFetchTask; + } + } +} diff --git a/Parse/Internal/HttpClient/Portable/HttpClient.Portable.cs b/Parse/Infrastructure/Execution/UniversalWebClient.cs similarity index 51% rename from Parse/Internal/HttpClient/Portable/HttpClient.Portable.cs rename to Parse/Infrastructure/Execution/UniversalWebClient.cs index aa984811..30f07a69 100644 --- a/Parse/Internal/HttpClient/Portable/HttpClient.Portable.cs +++ b/Parse/Infrastructure/Execution/UniversalWebClient.cs @@ -1,78 +1,101 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. using System; +using System.Collections.Generic; using System.IO; using System.Net; +using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; -using System.Net.Http; -using System.Linq; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Execution; +using Parse.Infrastructure.Utilities; +using BCLWebClient = System.Net.Http.HttpClient; -using NetHttpClient = System.Net.Http.HttpClient; -using System.Net.Http.Headers; -using System.Collections.Generic; - -namespace Parse.Common.Internal +namespace Parse.Infrastructure.Execution { - public class HttpClient : IHttpClient + /// + /// A universal implementation of . + /// + public class UniversalWebClient : IWebClient { - private static HashSet HttpContentHeaders = new HashSet { { "Allow" }, { "Content-Disposition" }, { "Content-Encoding" }, { "Content-Language" }, { "Content-Length" }, { "Content-Location" }, { "Content-MD5" }, { "Content-Range" }, { "Content-Type" }, { "Expires" }, { "Last-Modified" } }; - - public HttpClient() : this(new NetHttpClient { }) { } - - public HttpClient(NetHttpClient client) => this.client = client; - - private NetHttpClient client; - - public Task> ExecuteAsync(HttpRequest httpRequest, IProgress uploadProgress, IProgress downloadProgress, CancellationToken cancellationToken) + static HashSet ContentHeaders { get; } = new HashSet { - uploadProgress = uploadProgress ?? new Progress { }; - downloadProgress = downloadProgress ?? new Progress { }; + { "Allow" }, + { "Content-Disposition" }, + { "Content-Encoding" }, + { "Content-Language" }, + { "Content-Length" }, + { "Content-Location" }, + { "Content-MD5" }, + { "Content-Range" }, + { "Content-Type" }, + { "Expires" }, + { "Last-Modified" } + }; + + public UniversalWebClient() : this(new BCLWebClient { }) { } + + public UniversalWebClient(BCLWebClient client) => Client = client; + + BCLWebClient Client { get; set; } + + public Task> ExecuteAsync(WebRequest httpRequest, IProgress uploadProgress, IProgress downloadProgress, CancellationToken cancellationToken) + { + uploadProgress ??= new Progress { }; + downloadProgress ??= new Progress { }; - HttpRequestMessage message = new HttpRequestMessage(new HttpMethod(httpRequest.Method), httpRequest.Uri); + HttpRequestMessage message = new HttpRequestMessage(new HttpMethod(httpRequest.Method), httpRequest.Target); // Fill in zero-length data if method is post. - Stream data = httpRequest.Data == null && httpRequest.Method.ToLower().Equals("post") ? new MemoryStream(new byte[0]) : httpRequest.Data; - if (data != null) + if ((httpRequest.Data is null && httpRequest.Method.ToLower().Equals("post") ? new MemoryStream(new byte[0]) : httpRequest.Data) is Stream { } data) + { message.Content = new StreamContent(data); + } if (httpRequest.Headers != null) { - foreach (var header in httpRequest.Headers) + foreach (KeyValuePair header in httpRequest.Headers) { - if (HttpContentHeaders.Contains(header.Key)) + if (ContentHeaders.Contains(header.Key)) + { message.Content.Headers.Add(header.Key, header.Value); + } else + { message.Headers.Add(header.Key, header.Value); + } } } // Avoid aggressive caching on Windows Phone 8.1. + message.Headers.Add("Cache-Control", "no-cache"); message.Headers.IfModifiedSince = DateTimeOffset.UtcNow; // TODO: (richardross) investigate progress here, maybe there's something we're missing in order to support this. - uploadProgress.Report(new ParseUploadProgressEventArgs { Progress = 0 }); - return client.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ContinueWith(httpMessageTask => + uploadProgress.Report(new DataTransferLevel { Amount = 0 }); + + return Client.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ContinueWith(httpMessageTask => { - var response = httpMessageTask.Result; - uploadProgress.Report(new ParseUploadProgressEventArgs { Progress = 1 }); + HttpResponseMessage response = httpMessageTask.Result; + uploadProgress.Report(new DataTransferLevel { Amount = 1 }); return response.Content.ReadAsStreamAsync().ContinueWith(streamTask => { - var resultStream = new MemoryStream(); - var responseStream = streamTask.Result; + MemoryStream resultStream = new MemoryStream { }; + Stream responseStream = streamTask.Result; - int bufferSize = 4096; + int bufferSize = 4096, bytesRead = 0; byte[] buffer = new byte[bufferSize]; - int bytesRead = 0; - long totalLength = -1; - long readSoFar = 0; + long totalLength = -1, readSoFar = 0; try - { totalLength = responseStream.Length; } + { + totalLength = responseStream.Length; + } catch { }; return InternalExtensions.WhileAsync(() => responseStream.ReadAsync(buffer, 0, bufferSize, cancellationToken).OnSuccess(readTask => (bytesRead = readTask.Result) > 0), () => @@ -85,7 +108,9 @@ public Task> ExecuteAsync(HttpRequest httpRequest, readSoFar += bytesRead; if (totalLength > -1) - downloadProgress.Report(new ParseDownloadProgressEventArgs { Progress = 1.0 * readSoFar / totalLength }); + { + downloadProgress.Report(new DataTransferLevel { Amount = 1.0 * readSoFar / totalLength }); + } }); }).ContinueWith(_ => { @@ -94,11 +119,17 @@ public Task> ExecuteAsync(HttpRequest httpRequest, }).Unwrap().OnSuccess(_ => { // If getting stream size is not supported, then report download only once. + if (totalLength == -1) - downloadProgress.Report(new ParseDownloadProgressEventArgs { Progress = 1.0 }); - var resultAsArray = resultStream.ToArray(); + { + downloadProgress.Report(new DataTransferLevel { Amount = 1.0 }); + } + + byte[] resultAsArray = resultStream.ToArray(); resultStream.Dispose(); + // Assume UTF-8 encoding. + return new Tuple(response.StatusCode, Encoding.UTF8.GetString(resultAsArray, 0, resultAsArray.Length)); }); }); diff --git a/Parse/Internal/HttpClient/HttpRequest.cs b/Parse/Infrastructure/Execution/WebRequest.cs similarity index 80% rename from Parse/Internal/HttpClient/HttpRequest.cs rename to Parse/Infrastructure/Execution/WebRequest.cs index f1c4fc14..e4a3b059 100644 --- a/Parse/Internal/HttpClient/HttpRequest.cs +++ b/Parse/Infrastructure/Execution/WebRequest.cs @@ -4,14 +4,19 @@ using System.Collections.Generic; using System.IO; -namespace Parse.Common.Internal +namespace Parse.Infrastructure.Execution { /// /// IHttpRequest is an interface that provides an API to execute HTTP request data. /// - public class HttpRequest + public class WebRequest { - public Uri Uri { get; set; } + public Uri Target => new Uri(new Uri(Resource), Path); + + public string Resource { get; set; } + + public string Path { get; set; } + public IList> Headers { get; set; } /// diff --git a/Parse/Infrastructure/HostManifestData.cs b/Parse/Infrastructure/HostManifestData.cs new file mode 100644 index 00000000..75e38b86 --- /dev/null +++ b/Parse/Infrastructure/HostManifestData.cs @@ -0,0 +1,62 @@ +using System; +using System.Reflection; +using Parse.Abstractions.Infrastructure; + +namespace Parse.Infrastructure +{ + /// + /// In the event that you would like to use the Parse SDK + /// from a completely portable project, with no platform-specific library required, + /// to get full access to all of our features available on Parse Dashboard + /// (A/B testing, slow queries, etc.), you must set the values of this struct + /// to be appropriate for your platform. + /// + /// Any values set here will overwrite those that are automatically configured by + /// any platform-specific migration library your app includes. + /// + public class HostManifestData : IHostManifestData + { + /// + /// An instance of with inferred values based on the entry assembly. + /// + /// Should not be used with Unity. + public static HostManifestData Inferred => new HostManifestData + { + Version = Assembly.GetEntryAssembly().GetCustomAttribute().InformationalVersion, + Name = Assembly.GetEntryAssembly().GetCustomAttribute()?.Title ?? Assembly.GetEntryAssembly().GetCustomAttribute()?.Product ?? Assembly.GetEntryAssembly().GetName().Name, + ShortVersion = Assembly.GetEntryAssembly().GetName().Version.ToString(), + // TODO: For Xamarin, use manifest parsing, and for Unity, use some kind of package identifier API. + Identifier = AppDomain.CurrentDomain.FriendlyName + }; + + /// + /// The build version of your app. + /// + public string Version { get; set; } + + /// + /// The human-friendly display version number of your app. + /// + public string ShortVersion { get; set; } + + /// + /// The identifier of the application + /// + public string Identifier { get; set; } + + /// + /// The friendly name of your app. + /// + public string Name { get; set; } + + /// + /// Gets a value for whether or not this instance of is populated with default values. + /// + public bool IsDefault => Version is null && ShortVersion is null && Identifier is null && Name is null; + + /// + /// Gets a value for whether or not this instance of can currently be used for the generation of . + /// + public bool CanBeUsedForInference => !(IsDefault || String.IsNullOrWhiteSpace(ShortVersion)); + } +} diff --git a/Parse/Infrastructure/IdentifierBasedRelativeCacheLocationGenerator.cs b/Parse/Infrastructure/IdentifierBasedRelativeCacheLocationGenerator.cs new file mode 100644 index 00000000..0a076266 --- /dev/null +++ b/Parse/Infrastructure/IdentifierBasedRelativeCacheLocationGenerator.cs @@ -0,0 +1,44 @@ +using System; +using System.IO; +using Parse.Abstractions.Infrastructure; + +namespace Parse.Infrastructure +{ + /// + /// A configuration of the Parse SDK persistent storage location based on an identifier. + /// + public struct IdentifierBasedRelativeCacheLocationGenerator : IRelativeCacheLocationGenerator + { + internal static IdentifierBasedRelativeCacheLocationGenerator Fallback { get; } = new IdentifierBasedRelativeCacheLocationGenerator { IsFallback = true }; + + /// + /// Dictates whether or not this instance should act as a fallback for when has not yet been initialized but the storage path is needed. + /// + internal bool IsFallback { get; set; } + + /// + /// The identifier that all Parse SDK cache files should be labelled with. + /// + public string Identifier { get; set; } + + /// + /// The corresponding relative path generated by this . + /// + /// This will cause a .cachefile file extension to be added to the cache file in order to prevent the creation of files with unwanted extensions due to the value of containing periods. + public string GetRelativeCacheFilePath(IServiceHub serviceHub) + { + FileInfo file; + + while ((file = serviceHub.CacheController.GetRelativeFile(GeneratePath())).Exists && IsFallback) + ; + + return file.FullName; + } + + /// + /// Generates a path for use in the method. + /// + /// A potential path to the cachefile + string GeneratePath() => Path.Combine(nameof(Parse), IsFallback ? "_fallback" : "_global", $"{(IsFallback ? new Random { }.Next().ToString() : Identifier)}.cachefile"); + } +} diff --git a/Parse/Infrastructure/LateInitializedMutableServiceHub.cs b/Parse/Infrastructure/LateInitializedMutableServiceHub.cs new file mode 100644 index 00000000..a5e58e4e --- /dev/null +++ b/Parse/Infrastructure/LateInitializedMutableServiceHub.cs @@ -0,0 +1,165 @@ +using System; +using Parse.Abstractions.Infrastructure.Data; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Execution; +using Parse.Abstractions.Platform.Installations; +using Parse.Abstractions.Platform.Objects; +using Parse.Abstractions.Platform.Cloud; +using Parse.Abstractions.Platform.Configuration; +using Parse.Abstractions.Platform.Files; +using Parse.Abstractions.Platform.Push; +using Parse.Abstractions.Platform.Queries; +using Parse.Abstractions.Platform.Sessions; +using Parse.Abstractions.Platform.Users; +using Parse.Abstractions.Platform.Analytics; +using Parse.Infrastructure.Execution; +using Parse.Platform.Objects; +using Parse.Platform.Installations; +using Parse.Platform.Cloud; +using Parse.Platform.Configuration; +using Parse.Platform.Files; +using Parse.Platform.Queries; +using Parse.Platform.Sessions; +using Parse.Platform.Users; +using Parse.Platform.Analytics; +using Parse.Platform.Push; +using Parse.Infrastructure.Data; +using Parse.Infrastructure.Utilities; + +namespace Parse.Infrastructure +{ + public class LateInitializedMutableServiceHub : IMutableServiceHub + { + LateInitializer LateInitializer { get; } = new LateInitializer { }; + + public IServiceHubCloner Cloner { get; set; } + + public IMetadataController MetadataController + { + get => LateInitializer.GetValue(() => new MetadataController { EnvironmentData = EnvironmentData.Inferred, HostManifestData = HostManifestData.Inferred }); + set => LateInitializer.SetValue(value); + } + + public IWebClient WebClient + { + get => LateInitializer.GetValue(() => new UniversalWebClient { }); + set => LateInitializer.SetValue(value); + } + + public ICacheController CacheController + { + get => LateInitializer.GetValue(() => new CacheController { }); + set => LateInitializer.SetValue(value); + } + + public IParseObjectClassController ClassController + { + get => LateInitializer.GetValue(() => new ParseObjectClassController { }); + set => LateInitializer.SetValue(value); + } + + public IParseInstallationController InstallationController + { + get => LateInitializer.GetValue(() => new ParseInstallationController(CacheController)); + set => LateInitializer.SetValue(value); + } + + public IParseCommandRunner CommandRunner + { + get => LateInitializer.GetValue(() => new ParseCommandRunner(WebClient, InstallationController, MetadataController, ServerConnectionData, new Lazy(() => UserController))); + set => LateInitializer.SetValue(value); + } + + public IParseCloudCodeController CloudCodeController + { + get => LateInitializer.GetValue(() => new ParseCloudCodeController(CommandRunner, Decoder)); + set => LateInitializer.SetValue(value); + } + + public IParseConfigurationController ConfigurationController + { + get => LateInitializer.GetValue(() => new ParseConfigurationController(CommandRunner, CacheController, Decoder)); + set => LateInitializer.SetValue(value); + } + + public IParseFileController FileController + { + get => LateInitializer.GetValue(() => new ParseFileController(CommandRunner)); + set => LateInitializer.SetValue(value); + } + + public IParseObjectController ObjectController + { + get => LateInitializer.GetValue(() => new ParseObjectController(CommandRunner, Decoder, ServerConnectionData)); + set => LateInitializer.SetValue(value); + } + + public IParseQueryController QueryController + { + get => LateInitializer.GetValue(() => new ParseQueryController(CommandRunner, Decoder)); + set => LateInitializer.SetValue(value); + } + + public IParseSessionController SessionController + { + get => LateInitializer.GetValue(() => new ParseSessionController(CommandRunner, Decoder)); + set => LateInitializer.SetValue(value); + } + + public IParseUserController UserController + { + get => LateInitializer.GetValue(() => new ParseUserController(CommandRunner, Decoder)); + set => LateInitializer.SetValue(value); + } + + public IParseCurrentUserController CurrentUserController + { + get => LateInitializer.GetValue(() => new ParseCurrentUserController(CacheController, ClassController, Decoder)); + set => LateInitializer.SetValue(value); + } + + public IParseAnalyticsController AnalyticsController + { + get => LateInitializer.GetValue(() => new ParseAnalyticsController(CommandRunner)); + set => LateInitializer.SetValue(value); + } + + public IParseInstallationCoder InstallationCoder + { + get => LateInitializer.GetValue(() => new ParseInstallationCoder(Decoder, ClassController)); + set => LateInitializer.SetValue(value); + } + + public IParsePushChannelsController PushChannelsController + { + get => LateInitializer.GetValue(() => new ParsePushChannelsController(CurrentInstallationController)); + set => LateInitializer.SetValue(value); + } + + public IParsePushController PushController + { + get => LateInitializer.GetValue(() => new ParsePushController(CommandRunner, CurrentUserController)); + set => LateInitializer.SetValue(value); + } + + public IParseCurrentInstallationController CurrentInstallationController + { + get => LateInitializer.GetValue(() => new ParseCurrentInstallationController(InstallationController, CacheController, InstallationCoder, ClassController)); + set => LateInitializer.SetValue(value); + } + + public IParseDataDecoder Decoder + { + get => LateInitializer.GetValue(() => new ParseDataDecoder(ClassController)); + set => LateInitializer.SetValue(value); + } + + public IParseInstallationDataFinalizer InstallationDataFinalizer + { + get => LateInitializer.GetValue(() => new ParseInstallationDataFinalizer { }); + set => LateInitializer.SetValue(value); + } + + public IServerConnectionData ServerConnectionData { get; set; } + } +} diff --git a/Parse/Infrastructure/MetadataBasedRelativeCacheLocationGenerator.cs b/Parse/Infrastructure/MetadataBasedRelativeCacheLocationGenerator.cs new file mode 100644 index 00000000..9eadc5d1 --- /dev/null +++ b/Parse/Infrastructure/MetadataBasedRelativeCacheLocationGenerator.cs @@ -0,0 +1,37 @@ +using System.IO; +using System.Reflection; +using Parse.Abstractions.Infrastructure; + +namespace Parse.Infrastructure +{ + /// + /// A configuration of the Parse SDK persistent storage location based on product metadata such as company name and product name. + /// + public struct MetadataBasedRelativeCacheLocationGenerator : IRelativeCacheLocationGenerator + { + /// + /// An instance of with inferred values based on the entry assembly. Should be used with and . + /// + /// Should not be used with Unity. + public static MetadataBasedRelativeCacheLocationGenerator Inferred => new MetadataBasedRelativeCacheLocationGenerator + { + Company = Assembly.GetExecutingAssembly()?.GetCustomAttribute()?.Company, + Product = Assembly.GetEntryAssembly()?.GetCustomAttribute()?.Product ?? Assembly.GetEntryAssembly()?.GetName()?.Name + }; + + /// + /// The name of the company that owns the product specified by . + /// + public string Company { get; set; } + + /// + /// The name of the product that is using the Parse .NET SDK. + /// + public string Product { get; set; } + + /// + /// The corresponding relative path generated by this . + /// + public string GetRelativeCacheFilePath(IServiceHub serviceHub) => Path.Combine(Company ?? nameof(Parse), Product ?? "_global", $"{serviceHub.MetadataController.HostManifestData.ShortVersion ?? "1.0.0.0"}.pc"); + } +} diff --git a/Parse/Infrastructure/MetadataController.cs b/Parse/Infrastructure/MetadataController.cs new file mode 100644 index 00000000..6d8b83b3 --- /dev/null +++ b/Parse/Infrastructure/MetadataController.cs @@ -0,0 +1,17 @@ +using Parse.Abstractions.Infrastructure; + +namespace Parse.Infrastructure +{ + public class MetadataController : IMetadataController + { + /// + /// Information about your app. + /// + public IHostManifestData HostManifestData { get; set; } + + /// + /// Information about the environment the library is operating in. + /// + public IEnvironmentData EnvironmentData { get; set; } + } +} diff --git a/Parse/Infrastructure/MetadataMutator.cs b/Parse/Infrastructure/MetadataMutator.cs new file mode 100644 index 00000000..351f779b --- /dev/null +++ b/Parse/Infrastructure/MetadataMutator.cs @@ -0,0 +1,22 @@ +using Parse.Abstractions.Infrastructure; + +namespace Parse.Infrastructure +{ + /// + /// An for setting metadata information manually. + /// + public class MetadataMutator : MetadataController, IServiceHubMutator + { + /// + /// A value representing whether or not all of the required metadata information has been provided. + /// + public bool Valid => this is { EnvironmentData: { OSVersion: { }, Platform: { }, TimeZone: { } }, HostManifestData: { Identifier: { }, Name: { }, ShortVersion: { }, Version: { } } }; + + /// + /// Sets the to the instance. + /// + /// The to compose the information onto. + /// Thhe to use if a default service instance is required. + public void Mutate(ref IMutableServiceHub target, in IServiceHub referenceHub) => target.MetadataController = this; + } +} diff --git a/Parse/Infrastructure/MutableServiceHub.cs b/Parse/Infrastructure/MutableServiceHub.cs new file mode 100644 index 00000000..4c63669b --- /dev/null +++ b/Parse/Infrastructure/MutableServiceHub.cs @@ -0,0 +1,109 @@ +using System; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Data; +using Parse.Abstractions.Infrastructure.Execution; +using Parse.Abstractions.Platform.Analytics; +using Parse.Abstractions.Platform.Cloud; +using Parse.Abstractions.Platform.Configuration; +using Parse.Abstractions.Platform.Files; +using Parse.Abstractions.Platform.Installations; +using Parse.Abstractions.Platform.Objects; +using Parse.Abstractions.Platform.Push; +using Parse.Abstractions.Platform.Queries; +using Parse.Abstractions.Platform.Sessions; +using Parse.Abstractions.Platform.Users; +using Parse.Infrastructure.Data; +using Parse.Infrastructure.Execution; +using Parse.Platform.Analytics; +using Parse.Platform.Cloud; +using Parse.Platform.Configuration; +using Parse.Platform.Files; +using Parse.Platform.Installations; +using Parse.Platform.Objects; +using Parse.Platform.Push; +using Parse.Platform.Queries; +using Parse.Platform.Sessions; +using Parse.Platform.Users; + +namespace Parse.Infrastructure +{ + /// + /// A service hub that is mutable. + /// + /// This class is not thread safe; the mutability is allowed for the purposes of overriding values before it is used, as opposed to modifying it while it is in use. + public class MutableServiceHub : IMutableServiceHub + { + public IServerConnectionData ServerConnectionData { get; set; } + public IMetadataController MetadataController { get; set; } + + public IServiceHubCloner Cloner { get; set; } + + public IWebClient WebClient { get; set; } + public ICacheController CacheController { get; set; } + public IParseObjectClassController ClassController { get; set; } + + public IParseDataDecoder Decoder { get; set; } + + public IParseInstallationController InstallationController { get; set; } + public IParseCommandRunner CommandRunner { get; set; } + + public IParseCloudCodeController CloudCodeController { get; set; } + public IParseConfigurationController ConfigurationController { get; set; } + public IParseFileController FileController { get; set; } + public IParseObjectController ObjectController { get; set; } + public IParseQueryController QueryController { get; set; } + public IParseSessionController SessionController { get; set; } + public IParseUserController UserController { get; set; } + public IParseCurrentUserController CurrentUserController { get; set; } + + public IParseAnalyticsController AnalyticsController { get; set; } + + public IParseInstallationCoder InstallationCoder { get; set; } + + public IParsePushChannelsController PushChannelsController { get; set; } + public IParsePushController PushController { get; set; } + public IParseCurrentInstallationController CurrentInstallationController { get; set; } + public IParseInstallationDataFinalizer InstallationDataFinalizer { get; set; } + + public MutableServiceHub SetDefaults(IServerConnectionData connectionData = default) + { + ServerConnectionData ??= connectionData; + MetadataController ??= new MetadataController + { + EnvironmentData = EnvironmentData.Inferred, + HostManifestData = HostManifestData.Inferred + }; + + Cloner ??= new ConcurrentUserServiceHubCloner { }; + + WebClient ??= new UniversalWebClient { }; + CacheController ??= new CacheController { }; + ClassController ??= new ParseObjectClassController { }; + + Decoder ??= new ParseDataDecoder(ClassController); + + InstallationController ??= new ParseInstallationController(CacheController); + CommandRunner ??= new ParseCommandRunner(WebClient, InstallationController, MetadataController, ServerConnectionData, new Lazy(() => UserController)); + + CloudCodeController ??= new ParseCloudCodeController(CommandRunner, Decoder); + ConfigurationController ??= new ParseConfigurationController(CommandRunner, CacheController, Decoder); + FileController ??= new ParseFileController(CommandRunner); + ObjectController ??= new ParseObjectController(CommandRunner, Decoder, ServerConnectionData); + QueryController ??= new ParseQueryController(CommandRunner, Decoder); + SessionController ??= new ParseSessionController(CommandRunner, Decoder); + UserController ??= new ParseUserController(CommandRunner, Decoder); + CurrentUserController ??= new ParseCurrentUserController(CacheController, ClassController, Decoder); + + AnalyticsController ??= new ParseAnalyticsController(CommandRunner); + + InstallationCoder ??= new ParseInstallationCoder(Decoder, ClassController); + + PushController ??= new ParsePushController(CommandRunner, CurrentUserController); + CurrentInstallationController ??= new ParseCurrentInstallationController(InstallationController, CacheController, InstallationCoder, ClassController); + PushChannelsController ??= new ParsePushChannelsController(CurrentInstallationController); + InstallationDataFinalizer ??= new ParseInstallationDataFinalizer { }; + + return this; + } + } +} diff --git a/Parse/Infrastructure/OrchestrationServiceHub.cs b/Parse/Infrastructure/OrchestrationServiceHub.cs new file mode 100644 index 00000000..8aa99a9e --- /dev/null +++ b/Parse/Infrastructure/OrchestrationServiceHub.cs @@ -0,0 +1,69 @@ +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Data; +using Parse.Abstractions.Infrastructure.Execution; +using Parse.Abstractions.Platform.Analytics; +using Parse.Abstractions.Platform.Cloud; +using Parse.Abstractions.Platform.Configuration; +using Parse.Abstractions.Platform.Files; +using Parse.Abstractions.Platform.Installations; +using Parse.Abstractions.Platform.Objects; +using Parse.Abstractions.Platform.Push; +using Parse.Abstractions.Platform.Queries; +using Parse.Abstractions.Platform.Sessions; +using Parse.Abstractions.Platform.Users; + +namespace Parse.Infrastructure +{ + public class OrchestrationServiceHub : IServiceHub + { + public IServiceHub Default { get; set; } + + public IServiceHub Custom { get; set; } + + public IServiceHubCloner Cloner => Custom.Cloner ?? Default.Cloner; + + public IMetadataController MetadataController => Custom.MetadataController ?? Default.MetadataController; + + public IWebClient WebClient => Custom.WebClient ?? Default.WebClient; + + public ICacheController CacheController => Custom.CacheController ?? Default.CacheController; + + public IParseObjectClassController ClassController => Custom.ClassController ?? Default.ClassController; + + public IParseInstallationController InstallationController => Custom.InstallationController ?? Default.InstallationController; + + public IParseCommandRunner CommandRunner => Custom.CommandRunner ?? Default.CommandRunner; + + public IParseCloudCodeController CloudCodeController => Custom.CloudCodeController ?? Default.CloudCodeController; + + public IParseConfigurationController ConfigurationController => Custom.ConfigurationController ?? Default.ConfigurationController; + + public IParseFileController FileController => Custom.FileController ?? Default.FileController; + + public IParseObjectController ObjectController => Custom.ObjectController ?? Default.ObjectController; + + public IParseQueryController QueryController => Custom.QueryController ?? Default.QueryController; + + public IParseSessionController SessionController => Custom.SessionController ?? Default.SessionController; + + public IParseUserController UserController => Custom.UserController ?? Default.UserController; + + public IParseCurrentUserController CurrentUserController => Custom.CurrentUserController ?? Default.CurrentUserController; + + public IParseAnalyticsController AnalyticsController => Custom.AnalyticsController ?? Default.AnalyticsController; + + public IParseInstallationCoder InstallationCoder => Custom.InstallationCoder ?? Default.InstallationCoder; + + public IParsePushChannelsController PushChannelsController => Custom.PushChannelsController ?? Default.PushChannelsController; + + public IParsePushController PushController => Custom.PushController ?? Default.PushController; + + public IParseCurrentInstallationController CurrentInstallationController => Custom.CurrentInstallationController ?? Default.CurrentInstallationController; + + public IServerConnectionData ServerConnectionData => Custom.ServerConnectionData ?? Default.ServerConnectionData; + + public IParseDataDecoder Decoder => Custom.Decoder ?? Default.Decoder; + + public IParseInstallationDataFinalizer InstallationDataFinalizer => Custom.InstallationDataFinalizer ?? Default.InstallationDataFinalizer; + } +} diff --git a/Parse/Public/ParseClassNameAttribute.cs b/Parse/Infrastructure/ParseClassNameAttribute.cs similarity index 91% rename from Parse/Public/ParseClassNameAttribute.cs rename to Parse/Infrastructure/ParseClassNameAttribute.cs index c4c39726..af769056 100644 --- a/Parse/Public/ParseClassNameAttribute.cs +++ b/Parse/Infrastructure/ParseClassNameAttribute.cs @@ -1,10 +1,6 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Parse { diff --git a/Parse/Public/ParseException.cs b/Parse/Infrastructure/ParseFailureException.cs similarity index 97% rename from Parse/Public/ParseException.cs rename to Parse/Infrastructure/ParseFailureException.cs index 3e3f49ce..57389a0b 100644 --- a/Parse/Public/ParseException.cs +++ b/Parse/Infrastructure/ParseFailureException.cs @@ -2,12 +2,12 @@ using System; -namespace Parse +namespace Parse.Infrastructure { /// /// Exceptions that may occur when sending requests to Parse. /// - public class ParseException : Exception + public class ParseFailureException : Exception { /// /// Error codes that may be delivered in response to requests to Parse. @@ -259,11 +259,7 @@ public enum ErrorCode UnsupportedService = 252 } - internal ParseException(ErrorCode code, string message, Exception cause = null) - : base(message, cause) - { - this.Code = code; - } + internal ParseFailureException(ErrorCode code, string message, Exception cause = null) : base(message, cause) => Code = code; /// /// The Parse error code associated with the exception. diff --git a/Parse/Public/ParseFieldNameAttribute.cs b/Parse/Infrastructure/ParseFieldNameAttribute.cs similarity index 82% rename from Parse/Public/ParseFieldNameAttribute.cs rename to Parse/Infrastructure/ParseFieldNameAttribute.cs index 7581ddb0..1a5e6eb3 100644 --- a/Parse/Public/ParseFieldNameAttribute.cs +++ b/Parse/Infrastructure/ParseFieldNameAttribute.cs @@ -1,10 +1,6 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Parse { @@ -19,10 +15,7 @@ public sealed class ParseFieldNameAttribute : Attribute /// /// The name of the field on the ParseObject that the /// property represents. - public ParseFieldNameAttribute(string fieldName) - { - FieldName = fieldName; - } + public ParseFieldNameAttribute(string fieldName) => FieldName = fieldName; /// /// Gets the name of the field represented by this property. diff --git a/Parse/Infrastructure/RelativeCacheLocationMutator.cs b/Parse/Infrastructure/RelativeCacheLocationMutator.cs new file mode 100644 index 00000000..fe332424 --- /dev/null +++ b/Parse/Infrastructure/RelativeCacheLocationMutator.cs @@ -0,0 +1,32 @@ +using Parse.Abstractions.Infrastructure; + +namespace Parse.Infrastructure +{ + /// + /// An for the relative cache file location. This should be used if the relative cache file location is not created correctly by the SDK, such as platforms on which it is not possible to gather metadata about the client assembly, or ones on which is inaccsessible. + /// + public class RelativeCacheLocationMutator : IServiceHubMutator + { + /// + /// An implementation instance which creates a path that should be used as the -relative cache location. + /// + public IRelativeCacheLocationGenerator RelativeCacheLocationGenerator { get; set; } + + /// + /// + /// + public bool Valid => RelativeCacheLocationGenerator is { }; + + /// + /// + /// + /// + /// + public void Mutate(ref IMutableServiceHub target, in IServiceHub referenceHub) => target.CacheController = (target as IServiceHub).CacheController switch + { + null => new CacheController { RelativeCacheFilePath = RelativeCacheLocationGenerator.GetRelativeCacheFilePath(referenceHub) }, + IDiskFileCacheController { } controller => (Controller: controller, controller.RelativeCacheFilePath = RelativeCacheLocationGenerator.GetRelativeCacheFilePath(referenceHub)).Controller, + { } controller => controller + }; + } +} diff --git a/Parse/Infrastructure/ServerConnectionData.cs b/Parse/Infrastructure/ServerConnectionData.cs new file mode 100644 index 00000000..6cedaccc --- /dev/null +++ b/Parse/Infrastructure/ServerConnectionData.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using Parse.Abstractions.Infrastructure; + +namespace Parse.Infrastructure +{ + /// + /// Represents the configuration of the Parse SDK. + /// + public struct ServerConnectionData : IServerConnectionData + { + // TODO: Consider simplification of names: ApplicationID => Application | Target, ServerURI => Server, MasterKey => Master. + // TODO: Move Test property elsewhere. + + internal bool Test { get; set; } + + /// + /// The App ID of your app. + /// + public string ApplicationID { get; set; } + + /// + /// A URI pointing to the target Parse Server instance hosting the app targeted by . + /// + public string ServerURI { get; set; } + + /// + /// The .NET Key for the Parse app targeted by . + /// + public string Key { get; set; } + + /// + /// The Master Key for the Parse app targeted by . + /// + public string MasterKey { get; set; } + + // ALTERNATE NAME: AuxiliaryHeaders, AdditionalHeaders + + /// + /// Additional HTTP headers to be sent with network requests from the SDK. + /// + public IDictionary Headers { get; set; } + } +} diff --git a/Parse/Infrastructure/ServiceHub.cs b/Parse/Infrastructure/ServiceHub.cs new file mode 100644 index 00000000..564b7bf6 --- /dev/null +++ b/Parse/Infrastructure/ServiceHub.cs @@ -0,0 +1,73 @@ +using System; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Data; +using Parse.Abstractions.Infrastructure.Execution; +using Parse.Abstractions.Platform.Analytics; +using Parse.Abstractions.Platform.Cloud; +using Parse.Abstractions.Platform.Configuration; +using Parse.Abstractions.Platform.Files; +using Parse.Abstractions.Platform.Installations; +using Parse.Abstractions.Platform.Objects; +using Parse.Abstractions.Platform.Push; +using Parse.Abstractions.Platform.Queries; +using Parse.Abstractions.Platform.Sessions; +using Parse.Abstractions.Platform.Users; +using Parse.Infrastructure.Data; +using Parse.Infrastructure.Execution; +using Parse.Infrastructure.Utilities; +using Parse.Platform.Analytics; +using Parse.Platform.Cloud; +using Parse.Platform.Configuration; +using Parse.Platform.Files; +using Parse.Platform.Installations; +using Parse.Platform.Objects; +using Parse.Platform.Push; +using Parse.Platform.Queries; +using Parse.Platform.Sessions; +using Parse.Platform.Users; + +namespace Parse.Infrastructure +{ + + /// + /// A service hub that uses late initialization to efficiently provide controllers and other dependencies to internal Parse SDK systems. + /// + public class ServiceHub : IServiceHub + { + LateInitializer LateInitializer { get; } = new LateInitializer { }; + + public IServerConnectionData ServerConnectionData { get; set; } + public IMetadataController MetadataController => LateInitializer.GetValue(() => new MetadataController { HostManifestData = HostManifestData.Inferred, EnvironmentData = EnvironmentData.Inferred }); + + public IServiceHubCloner Cloner => LateInitializer.GetValue(() => new { } as object as IServiceHubCloner); + + public IWebClient WebClient => LateInitializer.GetValue(() => new UniversalWebClient { }); + public ICacheController CacheController => LateInitializer.GetValue(() => new CacheController { }); + public IParseObjectClassController ClassController => LateInitializer.GetValue(() => new ParseObjectClassController { }); + + public IParseDataDecoder Decoder => LateInitializer.GetValue(() => new ParseDataDecoder(ClassController)); + + public IParseInstallationController InstallationController => LateInitializer.GetValue(() => new ParseInstallationController(CacheController)); + public IParseCommandRunner CommandRunner => LateInitializer.GetValue(() => new ParseCommandRunner(WebClient, InstallationController, MetadataController, ServerConnectionData, new Lazy(() => UserController))); + + public IParseCloudCodeController CloudCodeController => LateInitializer.GetValue(() => new ParseCloudCodeController(CommandRunner, Decoder)); + public IParseConfigurationController ConfigurationController => LateInitializer.GetValue(() => new ParseConfigurationController(CommandRunner, CacheController, Decoder)); + public IParseFileController FileController => LateInitializer.GetValue(() => new ParseFileController(CommandRunner)); + public IParseObjectController ObjectController => LateInitializer.GetValue(() => new ParseObjectController(CommandRunner, Decoder, ServerConnectionData)); + public IParseQueryController QueryController => LateInitializer.GetValue(() => new ParseQueryController(CommandRunner, Decoder)); + public IParseSessionController SessionController => LateInitializer.GetValue(() => new ParseSessionController(CommandRunner, Decoder)); + public IParseUserController UserController => LateInitializer.GetValue(() => new ParseUserController(CommandRunner, Decoder)); + public IParseCurrentUserController CurrentUserController => LateInitializer.GetValue(() => new ParseCurrentUserController(CacheController, ClassController, Decoder)); + + public IParseAnalyticsController AnalyticsController => LateInitializer.GetValue(() => new ParseAnalyticsController(CommandRunner)); + + public IParseInstallationCoder InstallationCoder => LateInitializer.GetValue(() => new ParseInstallationCoder(Decoder, ClassController)); + + public IParsePushChannelsController PushChannelsController => LateInitializer.GetValue(() => new ParsePushChannelsController(CurrentInstallationController)); + public IParsePushController PushController => LateInitializer.GetValue(() => new ParsePushController(CommandRunner, CurrentUserController)); + public IParseCurrentInstallationController CurrentInstallationController => LateInitializer.GetValue(() => new ParseCurrentInstallationController(InstallationController, CacheController, InstallationCoder, ClassController)); + public IParseInstallationDataFinalizer InstallationDataFinalizer => LateInitializer.GetValue(() => new ParseInstallationDataFinalizer { }); + + public bool Reset() => LateInitializer.Used && LateInitializer.Reset(); + } +} diff --git a/Parse/Infrastructure/TransientCacheController.cs b/Parse/Infrastructure/TransientCacheController.cs new file mode 100644 index 00000000..ccfd62f7 --- /dev/null +++ b/Parse/Infrastructure/TransientCacheController.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure; +using static Parse.Resources; + +namespace Parse.Infrastructure +{ + public class TransientCacheController : ICacheController + { + class VirtualCache : Dictionary, IDataCache + { + public Task AddAsync(string key, object value) + { + Add(key, value); + return Task.CompletedTask; + } + + public Task RemoveAsync(string key) + { + Remove(key); + return Task.CompletedTask; + } + } + + VirtualCache Cache { get; } = new VirtualCache { }; + + public void Clear() => Cache.Clear(); + + public FileInfo GetRelativeFile(string path) => throw new NotSupportedException(TransientCacheControllerDiskFileOperationNotSupportedMessage); + + public Task> LoadAsync() => Task.FromResult>(Cache); + + public Task> SaveAsync(IDictionary contents) + { + foreach (KeyValuePair pair in contents) + { + ((IDictionary) Cache).Add(pair); + } + + return Task.FromResult>(Cache); + } + + public Task TransferAsync(string originFilePath, string targetFilePath) => Task.FromException(new NotSupportedException(TransientCacheControllerDiskFileOperationNotSupportedMessage)); + } +} diff --git a/Parse/AssemblyLister.cs b/Parse/Infrastructure/Utilities/AssemblyLister.cs similarity index 87% rename from Parse/AssemblyLister.cs rename to Parse/Infrastructure/Utilities/AssemblyLister.cs index 5ed14813..c3105903 100644 --- a/Parse/AssemblyLister.cs +++ b/Parse/Infrastructure/Utilities/AssemblyLister.cs @@ -1,9 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; -namespace AssemblyLister +namespace Parse.Infrastructure.Utilities { /// /// A class that lets you list all loaded assemblies in a PCL-compliant way. @@ -25,16 +25,14 @@ public static IEnumerable AllAssemblies private static IEnumerable DeepWalkReferences(this Assembly assembly, HashSet seen = null) { - seen = seen ?? new HashSet(); + seen ??= new HashSet(); if (!seen.Add(assembly.FullName)) - { return Enumerable.Empty(); - } List assemblies = new List { assembly }; - foreach (var reference in assembly.GetReferencedAssemblies()) + foreach (AssemblyName reference in assembly.GetReferencedAssemblies()) { if (seen.Contains(reference.FullName)) continue; diff --git a/Parse/Public/Utilities/Conversion.cs b/Parse/Infrastructure/Utilities/Conversion.cs similarity index 55% rename from Parse/Public/Utilities/Conversion.cs rename to Parse/Infrastructure/Utilities/Conversion.cs index 8e0686f1..fc870831 100644 --- a/Parse/Public/Utilities/Conversion.cs +++ b/Parse/Infrastructure/Utilities/Conversion.cs @@ -2,10 +2,11 @@ using System; using System.Collections.Generic; -using Parse.Common.Internal; -namespace Parse.Utilities +namespace Parse.Infrastructure.Utilities { +#warning Possibly should be refactored. + /// /// A set of utilities for converting generic types between each other. /// @@ -48,43 +49,23 @@ public static class Conversion internal static object ConvertTo(object value) { if (value is T || value == null) - { return value; - } - if (ReflectionHelpers.IsPrimitive(typeof(T))) - { + if (typeof(T).IsPrimitive) return (T) Convert.ChangeType(value, typeof(T), System.Globalization.CultureInfo.InvariantCulture); - } - if (ReflectionHelpers.IsConstructedGenericType(typeof(T))) + if (typeof(T).IsConstructedGenericType) { // Add lifting for nullables. Only supports conversions between primitives. - if (ReflectionHelpers.IsNullable(typeof(T))) - { - Type innerType = ReflectionHelpers.GetGenericTypeArguments(typeof(T))[0]; - if (ReflectionHelpers.IsPrimitive(innerType)) - { - return (T) Convert.ChangeType(value, innerType, System.Globalization.CultureInfo.InvariantCulture); - } - } - Type listType = GetInterfaceType(value.GetType(), typeof(IList<>)); - if (listType != null && - typeof(T).GetGenericTypeDefinition() == typeof(IList<>)) - { - Type wrapperType = typeof(FlexibleListWrapper<,>) - .MakeGenericType(ReflectionHelpers.GetGenericTypeArguments(typeof(T))[0], - ReflectionHelpers.GetGenericTypeArguments(listType)[0]); - return Activator.CreateInstance(wrapperType, value); - } - Type dictType = GetInterfaceType(value.GetType(), typeof(IDictionary<,>)); - if (dictType != null && typeof(T).GetGenericTypeDefinition() == typeof(IDictionary<,>)) - { - Type wrapperType = typeof(FlexibleDictionaryWrapper<,>) - .MakeGenericType(ReflectionHelpers.GetGenericTypeArguments(typeof(T))[1], - ReflectionHelpers.GetGenericTypeArguments(dictType)[1]); - return Activator.CreateInstance(wrapperType, value); - } + + if (typeof(T).CheckWrappedWithNullable() && typeof(T).GenericTypeArguments[0] is { IsPrimitive: true } innerType) + return (T) Convert.ChangeType(value, innerType, System.Globalization.CultureInfo.InvariantCulture); + + if (GetInterfaceType(value.GetType(), typeof(IList<>)) is { } listType && typeof(T).GetGenericTypeDefinition() == typeof(IList<>)) + return Activator.CreateInstance(typeof(FlexibleListWrapper<,>).MakeGenericType(typeof(T).GenericTypeArguments[0], listType.GenericTypeArguments[0]), value); + + if (GetInterfaceType(value.GetType(), typeof(IDictionary<,>)) is { } dictType && typeof(T).GetGenericTypeDefinition() == typeof(IDictionary<,>)) + return Activator.CreateInstance(typeof(FlexibleDictionaryWrapper<,>).MakeGenericType(typeof(T).GenericTypeArguments[1], dictType.GenericTypeArguments[1]), value); } return value; @@ -98,23 +79,20 @@ internal static object ConvertTo(object value) /// The map is: /// (object type, generic interface type) => constructed generic type /// - private static readonly Dictionary, Type> interfaceLookupCache = new Dictionary, Type>(); + static Dictionary, Type> InterfaceLookupCache { get; } = new Dictionary, Type>(); - private static Type GetInterfaceType(Type objType, Type genericInterfaceType) + static Type GetInterfaceType(Type objType, Type genericInterfaceType) { Tuple cacheKey = new Tuple(objType, genericInterfaceType); - if (interfaceLookupCache.ContainsKey(cacheKey)) - { - return interfaceLookupCache[cacheKey]; - } - foreach (Type type in ReflectionHelpers.GetInterfaces(objType)) - { - if (ReflectionHelpers.IsConstructedGenericType(type) && type.GetGenericTypeDefinition() == genericInterfaceType) - { - return interfaceLookupCache[cacheKey] = type; - } - } - return null; + + if (InterfaceLookupCache.ContainsKey(cacheKey)) + return InterfaceLookupCache[cacheKey]; + + foreach (Type type in objType.GetInterfaces()) + if (type.IsConstructedGenericType && type.GetGenericTypeDefinition() == genericInterfaceType) + return InterfaceLookupCache[cacheKey] = type; + + return default; } } } \ No newline at end of file diff --git a/Parse/Infrastructure/Utilities/FileUtilities.cs b/Parse/Infrastructure/Utilities/FileUtilities.cs new file mode 100644 index 00000000..43280b2b --- /dev/null +++ b/Parse/Infrastructure/Utilities/FileUtilities.cs @@ -0,0 +1,36 @@ +using System.IO; +using System.Text; +using System.Threading.Tasks; + +namespace Parse.Infrastructure.Utilities +{ + /// + /// A collection of utility methods and properties for writing to the app-specific persistent storage folder. + /// + internal static class FileUtilities + { + /// + /// Asynchronously read all of the little-endian 16-bit character units (UTF-16) contained within the file wrapped by the provided instance. + /// + /// The instance wrapping the target file that string content is to be read from + /// A task that should contain the little-endian 16-bit character string (UTF-16) extracted from the if the read completes successfully + public static async Task ReadAllTextAsync(this FileInfo file) + { + using StreamReader reader = new StreamReader(file.OpenRead(), Encoding.Unicode); + return await reader.ReadToEndAsync(); + } + + /// + /// Asynchronously writes the provided little-endian 16-bit character string to the file wrapped by the provided instance. + /// + /// The instance wrapping the target file that is to be written to + /// The little-endian 16-bit Unicode character string (UTF-16) that is to be written to the + /// A task that completes once the write operation to the completes + public static async Task WriteContentAsync(this FileInfo file, string content) + { + using FileStream stream = new FileStream(Path.GetFullPath(file.FullName), FileMode.Create, FileAccess.Write, FileShare.Read, 4096, FileOptions.SequentialScan | FileOptions.Asynchronous); + byte[] data = Encoding.Unicode.GetBytes(content); + await stream.WriteAsync(data, 0, data.Length); + } + } +} diff --git a/Parse/Infrastructure/Utilities/FlexibleDictionaryWrapper.cs b/Parse/Infrastructure/Utilities/FlexibleDictionaryWrapper.cs new file mode 100644 index 00000000..8f644275 --- /dev/null +++ b/Parse/Infrastructure/Utilities/FlexibleDictionaryWrapper.cs @@ -0,0 +1,77 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System.Collections.Generic; +using System.Linq; + +namespace Parse.Infrastructure.Utilities +{ + /// + /// Provides a Dictionary implementation that can delegate to any other + /// dictionary, regardless of its value type. Used for coercion of + /// dictionaries when returning them to users. + /// + /// The resulting type of value in the dictionary. + /// The original type of value in the dictionary. + [Preserve(AllMembers = true, Conditional = false)] + public class FlexibleDictionaryWrapper : IDictionary + { + private readonly IDictionary toWrap; + public FlexibleDictionaryWrapper(IDictionary toWrap) => this.toWrap = toWrap; + + public void Add(string key, TOut value) => toWrap.Add(key, (TIn) Conversion.ConvertTo(value)); + + public bool ContainsKey(string key) => toWrap.ContainsKey(key); + + public ICollection Keys => toWrap.Keys; + + public bool Remove(string key) => toWrap.Remove(key); + + public bool TryGetValue(string key, out TOut value) + { + bool result = toWrap.TryGetValue(key, out TIn outValue); + value = (TOut) Conversion.ConvertTo(outValue); + return result; + } + + public ICollection Values => toWrap.Values + .Select(item => (TOut) Conversion.ConvertTo(item)).ToList(); + + public TOut this[string key] + { + get => (TOut) Conversion.ConvertTo(toWrap[key]); + set => toWrap[key] = (TIn) Conversion.ConvertTo(value); + } + + public void Add(KeyValuePair item) => toWrap.Add(new KeyValuePair(item.Key, + (TIn) Conversion.ConvertTo(item.Value))); + + public void Clear() => toWrap.Clear(); + + public bool Contains(KeyValuePair item) => toWrap.Contains(new KeyValuePair(item.Key, + (TIn) Conversion.ConvertTo(item.Value))); + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + IEnumerable> converted = from pair in toWrap + select new KeyValuePair(pair.Key, + (TOut) Conversion.ConvertTo(pair.Value)); + converted.ToList().CopyTo(array, arrayIndex); + } + + public int Count => toWrap.Count; + + public bool IsReadOnly => toWrap.IsReadOnly; + + public bool Remove(KeyValuePair item) => toWrap.Remove(new KeyValuePair(item.Key, + (TIn) Conversion.ConvertTo(item.Value))); + + public IEnumerator> GetEnumerator() + { + foreach (KeyValuePair pair in toWrap) + yield return new KeyValuePair(pair.Key, + (TOut) Conversion.ConvertTo(pair.Value)); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/Parse/Infrastructure/Utilities/FlexibleListWrapper.cs b/Parse/Infrastructure/Utilities/FlexibleListWrapper.cs new file mode 100644 index 00000000..1a93d348 --- /dev/null +++ b/Parse/Infrastructure/Utilities/FlexibleListWrapper.cs @@ -0,0 +1,57 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Parse.Infrastructure.Utilities +{ + /// + /// Provides a List implementation that can delegate to any other + /// list, regardless of its value type. Used for coercion of + /// lists when returning them to users. + /// + /// The resulting type of value in the list. + /// The original type of value in the list. + [Preserve(AllMembers = true, Conditional = false)] + public class FlexibleListWrapper : IList + { + private IList toWrap; + public FlexibleListWrapper(IList toWrap) => this.toWrap = toWrap; + + public int IndexOf(TOut item) => toWrap.IndexOf((TIn) Conversion.ConvertTo(item)); + + public void Insert(int index, TOut item) => toWrap.Insert(index, (TIn) Conversion.ConvertTo(item)); + + public void RemoveAt(int index) => toWrap.RemoveAt(index); + + public TOut this[int index] + { + get => (TOut) Conversion.ConvertTo(toWrap[index]); + set => toWrap[index] = (TIn) Conversion.ConvertTo(value); + } + + public void Add(TOut item) => toWrap.Add((TIn) Conversion.ConvertTo(item)); + + public void Clear() => toWrap.Clear(); + + public bool Contains(TOut item) => toWrap.Contains((TIn) Conversion.ConvertTo(item)); + + public void CopyTo(TOut[] array, int arrayIndex) => toWrap.Select(item => (TOut) Conversion.ConvertTo(item)) + .ToList().CopyTo(array, arrayIndex); + + public int Count => toWrap.Count; + + public bool IsReadOnly => toWrap.IsReadOnly; + + public bool Remove(TOut item) => toWrap.Remove((TIn) Conversion.ConvertTo(item)); + + public IEnumerator GetEnumerator() + { + foreach (object item in (IEnumerable) toWrap) + yield return (TOut) Conversion.ConvertTo(item); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/Parse/Internal/Utilities/IdentityEqualityComparer.cs b/Parse/Infrastructure/Utilities/IdentityEqualityComparer.cs similarity index 73% rename from Parse/Internal/Utilities/IdentityEqualityComparer.cs rename to Parse/Infrastructure/Utilities/IdentityEqualityComparer.cs index 5517cbae..6bbbc0a6 100644 --- a/Parse/Internal/Utilities/IdentityEqualityComparer.cs +++ b/Parse/Infrastructure/Utilities/IdentityEqualityComparer.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; -namespace Parse.Common.Internal +namespace Parse.Infrastructure.Utilities { /// /// An equality comparer that uses the object identity (i.e. ReferenceEquals) @@ -12,14 +12,8 @@ namespace Parse.Common.Internal /// public class IdentityEqualityComparer : IEqualityComparer { - public bool Equals(T x, T y) - { - return ReferenceEquals(x, y); - } + public bool Equals(T x, T y) => ReferenceEquals(x, y); - public int GetHashCode(T obj) - { - return RuntimeHelpers.GetHashCode(obj); - } + public int GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj); } } diff --git a/Parse/Infrastructure/Utilities/InternalExtensions.cs b/Parse/Infrastructure/Utilities/InternalExtensions.cs new file mode 100644 index 00000000..164f2d13 --- /dev/null +++ b/Parse/Infrastructure/Utilities/InternalExtensions.cs @@ -0,0 +1,94 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.ExceptionServices; +using System.Threading.Tasks; + +namespace Parse.Infrastructure.Utilities +{ + /// + /// Provides helper methods that allow us to use terser code elsewhere. + /// + public static class InternalExtensions + { + /// + /// Ensures a task (even null) is awaitable. + /// + /// + /// + /// + public static Task Safe(this Task task) => task ?? Task.FromResult(default(T)); + + /// + /// Ensures a task (even null) is awaitable. + /// + /// + /// + public static Task Safe(this Task task) => task ?? Task.FromResult(null); + + public delegate void PartialAccessor(ref T arg); + + public static TValue GetOrDefault(this IDictionary self, + TKey key, + TValue defaultValue) + { + if (self.TryGetValue(key, out TValue value)) + return value; + return defaultValue; + } + + public static bool CollectionsEqual(this IEnumerable a, IEnumerable b) => Equals(a, b) || + a != null && b != null && + a.SequenceEqual(b); + + public static Task OnSuccess(this Task task, Func, TResult> continuation) => ((Task) task).OnSuccess(t => continuation((Task) t)); + + public static Task OnSuccess(this Task task, Action> continuation) => task.OnSuccess((Func, object>) (t => + { + continuation(t); + return null; + })); + + public static Task OnSuccess(this Task task, Func continuation) => task.ContinueWith(t => + { + if (t.IsFaulted) + { + AggregateException ex = t.Exception.Flatten(); + if (ex.InnerExceptions.Count == 1) + ExceptionDispatchInfo.Capture(ex.InnerExceptions[0]).Throw(); + else + ExceptionDispatchInfo.Capture(ex).Throw(); + // Unreachable + return Task.FromResult(default(TResult)); + } + else if (t.IsCanceled) + { + TaskCompletionSource tcs = new TaskCompletionSource(); + tcs.SetCanceled(); + return tcs.Task; + } + else + return Task.FromResult(continuation(t)); + }).Unwrap(); + + public static Task OnSuccess(this Task task, Action continuation) => task.OnSuccess((Func) (t => + { + continuation(t); + return null; + })); + + public static Task WhileAsync(Func> predicate, Func body) + { + Func iterate = null; + iterate = () => predicate().OnSuccess(t => + { + if (!t.Result) + return Task.FromResult(0); + return body().OnSuccess(_ => iterate()).Unwrap(); + }).Unwrap(); + return iterate(); + } + } +} diff --git a/Parse/Internal/Utilities/Json.cs b/Parse/Infrastructure/Utilities/JsonUtilities.cs similarity index 92% rename from Parse/Internal/Utilities/Json.cs rename to Parse/Infrastructure/Utilities/JsonUtilities.cs index 2005a14b..40c8da3c 100644 --- a/Parse/Internal/Utilities/Json.cs +++ b/Parse/Infrastructure/Utilities/JsonUtilities.cs @@ -3,19 +3,17 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Reflection; using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; -namespace Parse.Common.Internal +namespace Parse.Infrastructure.Utilities { /// /// A simple recursive-descent JSON Parser based on the grammar defined at http://www.json.org /// and http://tools.ietf.org/html/rfc4627 /// - public class Json + public class JsonUtilities { /// /// Place at the start of a regex to force the match to begin wherever the search starts (i.e. @@ -60,29 +58,21 @@ internal bool ParseObject(out object output) output = null; int initialCurrentIndex = CurrentIndex; if (!Accept(startObject)) - { return false; - } Dictionary dict = new Dictionary { }; while (true) { if (!ParseMember(out object pairValue)) - { break; - } Tuple pair = pairValue as Tuple; dict[pair.Item1] = pair.Item2; if (!Accept(valueSeparator)) - { break; - } } if (!Accept(endObject)) - { return false; - } output = dict; return true; } @@ -94,17 +84,11 @@ private bool ParseMember(out object output) { output = null; if (!ParseString(out object key)) - { return false; - } if (!Accept(nameSeparator)) - { return false; - } if (!ParseValue(out object value)) - { return false; - } output = new Tuple((string) key, value); return true; } @@ -116,26 +100,18 @@ internal bool ParseArray(out object output) { output = null; if (!Accept(startArray)) - { return false; - } List list = new List(); while (true) { if (!ParseValue(out object value)) - { break; - } list.Add(value); if (!Accept(valueSeparator)) - { break; - } } if (!Accept(endArray)) - { return false; - } output = list; return true; } @@ -174,16 +150,14 @@ private bool ParseString(out object output) { output = null; if (!Accept(stringValue, out Match m)) - { return false; - } // handle escapes: int offset = 0; Group contentCapture = m.Groups["content"]; StringBuilder builder = new StringBuilder(contentCapture.Value); foreach (Capture escape in m.Groups["escape"].Captures) { - int index = (escape.Index - contentCapture.Index) - offset; + int index = escape.Index - contentCapture.Index - offset; offset += escape.Length - 1; builder.Remove(index + 1, escape.Length - 1); switch (escape.Value[1]) @@ -231,9 +205,7 @@ private bool ParseNumber(out object output) { output = null; if (!Accept(numberValue, out Match m)) - { return false; - } if (m.Groups["frac"].Length > 0 || m.Groups["exp"].Length > 0) { // It's a double. @@ -254,9 +226,7 @@ private bool Accept(Regex matcher, out Match match) { match = matcher.Match(Input, CurrentIndex); if (match.Success) - { Skip(match.Length); - } return match.Success; } @@ -281,7 +251,7 @@ private bool Accept(char condition) ++currentStep; } - bool match = (currentStep < strLen) && (InputAsArray[currentStep] == condition); + bool match = currentStep < strLen && InputAsArray[currentStep] == condition; if (match) { ++step; @@ -326,19 +296,15 @@ private bool Accept(char[] condition) bool strMatch = true; for (int i = 0; currentStep < strLen && i < condition.Length; ++i, ++currentStep) - { if (InputAsArray[currentStep] != condition[i]) { strMatch = false; break; } - } - bool match = (currentStep < strLen) && strMatch; + bool match = currentStep < strLen && strMatch; if (match) - { Skip(step + condition.Length); - } return match; } } @@ -356,9 +322,7 @@ public static object Parse(string input) if ((parser.ParseObject(out object output) || parser.ParseArray(out output)) && parser.CurrentIndex == input.Length) - { return output; - } throw new ArgumentException("Input JSON was invalid."); } @@ -370,13 +334,9 @@ public static object Parse(string input) public static string Encode(IDictionary dict) { if (dict == null) - { throw new ArgumentNullException(); - } if (dict.Count == 0) - { return "{}"; - } StringBuilder builder = new StringBuilder("{"); foreach (KeyValuePair pair in dict) { @@ -397,13 +357,9 @@ public static string Encode(IDictionary dict) public static string Encode(IList list) { if (list == null) - { throw new ArgumentNullException(); - } if (list.Count == 0) - { return "[]"; - } StringBuilder builder = new StringBuilder("["); foreach (object item in list) { @@ -420,13 +376,9 @@ public static string Encode(IList list) public static string Encode(object obj) { if (obj is IDictionary dict) - { return Encode(dict); - } if (obj is IList list) - { return Encode(list); - } if (obj is string str) { str = escapePattern.Replace(str, m => @@ -454,17 +406,11 @@ public static string Encode(object obj) return "\"" + str + "\""; } if (obj is null) - { return "null"; - } if (obj is bool) - { return (bool) obj ? "true" : "false"; - } if (!obj.GetType().GetTypeInfo().IsPrimitive) - { throw new ArgumentException("Unable to encode objects of type " + obj.GetType()); - } return Convert.ToString(obj, CultureInfo.InvariantCulture); } } diff --git a/Parse/Infrastructure/Utilities/LateInitializer.cs b/Parse/Infrastructure/Utilities/LateInitializer.cs new file mode 100644 index 00000000..f3ddd129 --- /dev/null +++ b/Parse/Infrastructure/Utilities/LateInitializer.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Parse.Infrastructure.Utilities +{ + /// + /// A wrapper over a dictionary from value generator to value. Uses the fact that lambda expressions in a specific location are cached, so the cost of instantiating a generator delegate is only incurred once at the call site of and subsequent calls look up the result of the first generation from the dictionary based on the hash of the generator delegate. This is effectively a lazy initialization mechanism that allows the member type to remain unchanged. + /// + internal class LateInitializer + { + Lazy, object>> Storage { get; set; } = new Lazy, object>> { }; + + public TData GetValue(Func generator) + { + lock (generator) + { + if (Storage.IsValueCreated && Storage.Value.Keys.OfType>().FirstOrDefault() is { } key && Storage.Value.TryGetValue(key as Func, out object data)) + { + return (TData) data; + } + else + { + TData result = generator.Invoke(); + + Storage.Value.Add(generator as Func, result); + return result; + } + } + } + + public bool ClearValue() + { + lock (Storage) + { + if (Storage.IsValueCreated && Storage.Value.Keys.OfType>().FirstOrDefault() is { } key) + { + lock (key) + { + Storage.Value.Remove(key as Func); + return true; + } + } + } + + return false; + } + + public bool SetValue(TData value, bool initialize = true) + { + lock (Storage) + { + if (Storage.IsValueCreated && Storage.Value.Keys.OfType>().FirstOrDefault() is { } key) + { + lock (key) + { + Storage.Value[key as Func] = value; + return true; + } + } + else if (initialize) + { + Storage.Value[new Func(() => value) as Func] = value; + return true; + } + } + + return false; + } + + public bool Reset() + { + lock (Storage) + { + if (Storage.IsValueCreated) + { + Storage.Value.Clear(); + return true; + } + } + + return false; + } + + public bool Used => Storage.IsValueCreated; + } +} diff --git a/Parse/Internal/Utilities/LockSet.cs b/Parse/Infrastructure/Utilities/LockSet.cs similarity index 96% rename from Parse/Internal/Utilities/LockSet.cs rename to Parse/Infrastructure/Utilities/LockSet.cs index 03e5d549..659c538d 100644 --- a/Parse/Internal/Utilities/LockSet.cs +++ b/Parse/Infrastructure/Utilities/LockSet.cs @@ -4,7 +4,7 @@ using System.Runtime.CompilerServices; using System.Threading; -namespace Parse.Common.Internal +namespace Parse.Infrastructure.Utilities { public class LockSet { diff --git a/Parse/Infrastructure/Utilities/ReflectionUtilities.cs b/Parse/Infrastructure/Utilities/ReflectionUtilities.cs new file mode 100644 index 00000000..b5cf9d3d --- /dev/null +++ b/Parse/Infrastructure/Utilities/ReflectionUtilities.cs @@ -0,0 +1,43 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Parse.Infrastructure.Utilities +{ + public static class ReflectionUtilities + { + /// + /// Gets all of the defined constructors that aren't static on a given instance. + /// + /// + /// + public static IEnumerable GetInstanceConstructors(this Type type) => type.GetTypeInfo().DeclaredConstructors.Where(constructor => (constructor.Attributes & MethodAttributes.Static) == 0); + + /// + /// This method helps simplify the process of getting a constructor for a type. + /// A method like this exists in .NET but is not allowed in a Portable Class Library, + /// so we've built our own. + /// + /// + /// + /// + public static ConstructorInfo FindConstructor(this Type self, params Type[] parameterTypes) => self.GetConstructors().Where(constructor => constructor.GetParameters().Select(parameter => parameter.ParameterType).SequenceEqual(parameterTypes)).SingleOrDefault(); + + /// + /// Checks if a instance is another instance wrapped with . + /// + /// + /// + public static bool CheckWrappedWithNullable(this Type type) => type.IsConstructedGenericType && type.GetGenericTypeDefinition().Equals(typeof(Nullable<>)); + + /// + /// Gets the value of if the type has a custom attribute of type . + /// + /// + /// + public static string GetParseClassName(this Type type) => type.GetCustomAttribute()?.ClassName; + } +} diff --git a/Parse/Infrastructure/Utilities/SynchronizedEventHandler.cs b/Parse/Infrastructure/Utilities/SynchronizedEventHandler.cs new file mode 100644 index 00000000..468f25ac --- /dev/null +++ b/Parse/Infrastructure/Utilities/SynchronizedEventHandler.cs @@ -0,0 +1,73 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Parse.Infrastructure.Utilities +{ + /// + /// Represents an event handler that calls back from the synchronization context + /// that subscribed. + /// Should look like an EventArgs, but may not inherit EventArgs if T is implemented by the Windows team. + /// + public class SynchronizedEventHandler + { + LinkedList> Callbacks { get; } = new LinkedList> { }; + + public void Add(Delegate target) + { + lock (Callbacks) + { + TaskFactory factory = SynchronizationContext.Current is { } ? new TaskFactory(CancellationToken.None, TaskCreationOptions.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.FromCurrentSynchronizationContext()) : Task.Factory; + + foreach (Delegate invocation in target.GetInvocationList()) + { + Callbacks.AddLast(new Tuple(invocation, factory)); + } + } + } + + public void Remove(Delegate target) + { + lock (Callbacks) + { + if (Callbacks.Count == 0) + { + return; + } + + foreach (Delegate invocation in target.GetInvocationList()) + { + LinkedListNode> node = Callbacks.First; + + while (node != null) + { + if (node.Value.Item1 == invocation) + { + Callbacks.Remove(node); + break; + } + node = node.Next; + } + } + } + } + + public Task Invoke(object sender, T args) + { + IEnumerable> toInvoke; + Task[] toContinue = new[] { Task.FromResult(0) }; + + lock (Callbacks) + { + toInvoke = Callbacks.ToList(); + } + + List> invocations = toInvoke.Select(callback => callback.Item2.ContinueWhenAll(toContinue, _ => callback.Item1.DynamicInvoke(sender, args))).ToList(); + return Task.WhenAll(invocations); + } + } +} diff --git a/Parse/Internal/Utilities/TaskQueue.cs b/Parse/Infrastructure/Utilities/TaskQueue.cs similarity index 86% rename from Parse/Internal/Utilities/TaskQueue.cs rename to Parse/Infrastructure/Utilities/TaskQueue.cs index b39daac8..7710a071 100644 --- a/Parse/Internal/Utilities/TaskQueue.cs +++ b/Parse/Infrastructure/Utilities/TaskQueue.cs @@ -1,11 +1,10 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace Parse.Common.Internal +namespace Parse.Infrastructure.Utilities { /// /// A helper class for enqueuing tasks @@ -16,8 +15,7 @@ public class TaskQueue /// We only need to keep the tail of the queue. Cancelled tasks will /// just complete normally/immediately when their turn arrives. /// - private Task tail; - private readonly object mutex = new object(); + Task Tail { get; set; } /// /// Gets a cancellable task that can be safely awaited and is dependent @@ -31,10 +29,9 @@ public class TaskQueue /// A new task that should be awaited by enqueued tasks. private Task GetTaskToAwait(CancellationToken cancellationToken) { - lock (mutex) + lock (Mutex) { - Task toAwait = tail ?? Task.FromResult(true); - return toAwait.ContinueWith(task => { }, cancellationToken); + return (Tail ?? Task.FromResult(true)).ContinueWith(task => { }, cancellationToken); } } @@ -54,9 +51,11 @@ public T Enqueue(Func taskStart, CancellationToken cancellationToken { Task oldTail; T task; - lock (mutex) + + lock (Mutex) { - oldTail = tail ?? Task.FromResult(true); + oldTail = Tail ?? Task.FromResult(true); + // The task created by taskStart is responsible for waiting the // task passed to it before doing its work (this gives it an opportunity // to do startup work or save state before waiting for its turn in the queue @@ -65,11 +64,11 @@ public T Enqueue(Func taskStart, CancellationToken cancellationToken // The tail task should be dependent on the old tail as well as the newly-created // task. This prevents cancellation of the new task from causing the queue to run // out of order. - tail = Task.WhenAll(oldTail, task); + Tail = Task.WhenAll(oldTail, task); } return task; } - public object Mutex => mutex; + public object Mutex { get; } = new object { }; } } diff --git a/Parse/Internal/Utilities/ThreadingUtilities.cs b/Parse/Infrastructure/Utilities/ThreadingUtilities.cs similarity index 84% rename from Parse/Internal/Utilities/ThreadingUtilities.cs rename to Parse/Infrastructure/Utilities/ThreadingUtilities.cs index 5edca411..90ecd537 100644 --- a/Parse/Internal/Utilities/ThreadingUtilities.cs +++ b/Parse/Infrastructure/Utilities/ThreadingUtilities.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; -using System.Text; -namespace Parse.Internal.Utilities +namespace Parse.Infrastructure.Utilities { internal static class ThreadingUtilities { diff --git a/Parse/Infrastructure/Utilities/XamarinAttributes.cs b/Parse/Infrastructure/Utilities/XamarinAttributes.cs new file mode 100644 index 00000000..108bba23 --- /dev/null +++ b/Parse/Infrastructure/Utilities/XamarinAttributes.cs @@ -0,0 +1,407 @@ +using System; +using System.Collections.Generic; + +namespace Parse.Infrastructure.Utilities +{ + /// + /// A reimplementation of Xamarin's PreserveAttribute. + /// This allows us to support AOT and linking for Xamarin platforms. + /// + [AttributeUsage(AttributeTargets.All)] + internal class PreserveAttribute : Attribute + { + public bool AllMembers; + public bool Conditional; + } + + [AttributeUsage(AttributeTargets.All)] + internal class LinkerSafeAttribute : Attribute + { + public LinkerSafeAttribute() { } + } + + [Preserve(AllMembers = true)] + internal class PreserveWrapperTypes + { + /// + /// Exists to ensure that generic types are AOT-compiled for the conversions we support. + /// Any new value types that we add support for will need to be registered here. + /// The method itself is never called, but by virtue of the Preserve attribute being set + /// on the class, these types will be AOT-compiled. + /// + /// This also applies to Unity. + /// + static List AOTPreservations => new List + { + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleListWrapper), + typeof(FlexibleListWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper), + + typeof(FlexibleDictionaryWrapper), + typeof(FlexibleDictionaryWrapper) + }; + } +} diff --git a/Parse/Internal/Analytics/ParseAnalyticsPlugins.cs b/Parse/Internal/Analytics/ParseAnalyticsPlugins.cs deleted file mode 100644 index a700efb7..00000000 --- a/Parse/Internal/Analytics/ParseAnalyticsPlugins.cs +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using Parse.Core.Internal; - -namespace Parse.Analytics.Internal -{ - public class ParseAnalyticsPlugins : IParseAnalyticsPlugins - { - private static readonly object instanceMutex = new object(); - private static IParseAnalyticsPlugins instance; - public static IParseAnalyticsPlugins Instance - { - get - { - lock (instanceMutex) - { - instance = instance ?? new ParseAnalyticsPlugins(); - return instance; - } - } - set - { - lock (instanceMutex) - { - instance = value; - } - } - } - - private readonly object mutex = new object(); - - private IParseCorePlugins corePlugins; - private IParseAnalyticsController analyticsController; - - public void Reset() - { - lock (mutex) - { - CorePlugins = null; - AnalyticsController = null; - } - } - - public IParseCorePlugins CorePlugins - { - get - { - lock (mutex) - { - corePlugins = corePlugins ?? ParseCorePlugins.Instance; - return corePlugins; - } - } - set - { - lock (mutex) - { - corePlugins = value; - } - } - } - - public IParseAnalyticsController AnalyticsController - { - get - { - lock (mutex) - { - analyticsController = analyticsController ?? new ParseAnalyticsController(CorePlugins.CommandRunner); - return analyticsController; - } - } - set - { - lock (mutex) - { - analyticsController = value; - } - } - } - } -} \ No newline at end of file diff --git a/Parse/Internal/Cloud/Controller/ParseCloudCodeController.cs b/Parse/Internal/Cloud/Controller/ParseCloudCodeController.cs deleted file mode 100644 index 5248750e..00000000 --- a/Parse/Internal/Cloud/Controller/ParseCloudCodeController.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Parse.Utilities; -using Parse.Common.Internal; - -namespace Parse.Core.Internal -{ - public class ParseCloudCodeController : IParseCloudCodeController - { - private readonly IParseCommandRunner commandRunner; - - public ParseCloudCodeController(IParseCommandRunner commandRunner) - { - this.commandRunner = commandRunner; - } - - public Task CallFunctionAsync(string name, - IDictionary parameters, - string sessionToken, - CancellationToken cancellationToken) - { - var command = new ParseCommand(string.Format("functions/{0}", Uri.EscapeUriString(name)), - method: "POST", - sessionToken: sessionToken, - data: NoObjectsEncoder.Instance.Encode(parameters) as IDictionary); - - return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => - { - var decoded = ParseDecoder.Instance.Decode(t.Result.Item2) as IDictionary; - if (!decoded.ContainsKey("result")) - { - return default(T); - } - return Conversion.To(decoded["result"]); - }); - } - } -} diff --git a/Parse/Internal/Command/ParseCommand.cs b/Parse/Internal/Command/ParseCommand.cs deleted file mode 100644 index d63eb448..00000000 --- a/Parse/Internal/Command/ParseCommand.cs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using Parse.Common.Internal; -using System.Linq; - -namespace Parse.Core.Internal -{ - /// - /// ParseCommand is an with pre-populated - /// headers. - /// - public class ParseCommand : HttpRequest - { - public IDictionary DataObject { get; private set; } - public override Stream Data - { - get - { - if (base.Data != null) - { - return base.Data; - } - - return base.Data = (DataObject != null - ? new MemoryStream(Encoding.UTF8.GetBytes(Json.Encode(DataObject))) - : null); - } - set { base.Data = value; } - } - - public ParseCommand(string relativeUri, - string method, - string sessionToken = null, - IList> headers = null, - IDictionary data = null) : this(relativeUri: relativeUri, - method: method, - sessionToken: sessionToken, - headers: headers, - stream: null, - contentType: data != null ? "application/json" : null) - { - DataObject = data; - } - - public ParseCommand(string relativeUri, - string method, - string sessionToken = null, - IList> headers = null, - Stream stream = null, - string contentType = null) - { - Uri = new Uri(new Uri(ParseClient.CurrentConfiguration.ServerURI), relativeUri); - Method = method; - Data = stream; - Headers = new List>(headers ?? Enumerable.Empty>()); - - if (!string.IsNullOrEmpty(sessionToken)) - { - Headers.Add(new KeyValuePair("X-Parse-Session-Token", sessionToken)); - } - if (!string.IsNullOrEmpty(contentType)) - { - Headers.Add(new KeyValuePair("Content-Type", contentType)); - } - } - - public ParseCommand(ParseCommand other) - { - this.Uri = other.Uri; - this.Method = other.Method; - this.DataObject = other.DataObject; - this.Headers = new List>(other.Headers); - this.Data = other.Data; - } - } -} diff --git a/Parse/Internal/Command/ParseCommandRunner.cs b/Parse/Internal/Command/ParseCommandRunner.cs deleted file mode 100644 index ed307b65..00000000 --- a/Parse/Internal/Command/ParseCommandRunner.cs +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Collections.Generic; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using Parse.Common.Internal; - -namespace Parse.Core.Internal -{ - /// - /// The command runner for all SDK operations that need to interact with the targeted deployment of Parse Server. - /// - public class ParseCommandRunner : IParseCommandRunner - { - private readonly IHttpClient httpClient; - private readonly IInstallationIdController installationIdController; - - /// - /// Creates a new Parse SDK command runner. - /// - /// The implementation instance to use. - /// The implementation instance to use. - public ParseCommandRunner(IHttpClient httpClient, IInstallationIdController installationIdController) - { - this.httpClient = httpClient; - this.installationIdController = installationIdController; - } - - /// - /// Runs a specified . - /// - /// The to run. - /// An instance to push upload progress data to. - /// An instance to push download progress data to. - /// An asynchronous operation cancellation token that dictates if and when the operation should be cancelled. - /// - public Task>> RunCommandAsync(ParseCommand command, IProgress uploadProgress = null, IProgress downloadProgress = null, CancellationToken cancellationToken = default) - { - return PrepareCommand(command).ContinueWith(commandTask => - { - return httpClient.ExecuteAsync(commandTask.Result, uploadProgress, downloadProgress, cancellationToken).OnSuccess(t => - { - cancellationToken.ThrowIfCancellationRequested(); - - Tuple response = t.Result; - string contentString = response.Item2; - int responseCode = (int) response.Item1; - if (responseCode >= 500) - { - // Server error, return InternalServerError. - throw new ParseException(ParseException.ErrorCode.InternalServerError, response.Item2); - } - else if (contentString != null) - { - IDictionary contentJson = null; - try - { - // TODO: Newer versions of Parse Server send the failure results back as HTML. - contentJson = contentString.StartsWith("[") - ? new Dictionary { ["results"] = Json.Parse(contentString) } - : Json.Parse(contentString) as IDictionary; - } - catch (Exception e) - { - throw new ParseException(ParseException.ErrorCode.OtherCause, "Invalid or alternatively-formatted response recieved from server.", e); - } - if (responseCode < 200 || responseCode > 299) - { - int code = (int) (contentJson.ContainsKey("code") ? (long) contentJson["code"] : (int) ParseException.ErrorCode.OtherCause); - string error = contentJson.ContainsKey("error") ? - contentJson["error"] as string : - contentString; - throw new ParseException((ParseException.ErrorCode) code, error); - } - return new Tuple>(response.Item1, contentJson); - } - return new Tuple>(response.Item1, null); - }); - }).Unwrap(); - } - - private const string revocableSessionTokentrueValue = "1"; - private Task PrepareCommand(ParseCommand command) - { - ParseCommand newCommand = new ParseCommand(command); - - Task installationIdTask = installationIdController.GetAsync().ContinueWith(t => - { - newCommand.Headers.Add(new KeyValuePair("X-Parse-Installation-Id", t.Result.ToString())); - return newCommand; - }); - - // TODO (richardross): Inject configuration instead of using shared static here. - ParseClient.Configuration configuration = ParseClient.CurrentConfiguration; - newCommand.Headers.Add(new KeyValuePair("X-Parse-Application-Id", configuration.ApplicationID)); - newCommand.Headers.Add(new KeyValuePair("X-Parse-Client-Version", ParseClient.VersionString)); - - if (configuration.AuxiliaryHeaders != null) - { - foreach (KeyValuePair header in configuration.AuxiliaryHeaders) - { - newCommand.Headers.Add(header); - } - } - - if (!String.IsNullOrEmpty(configuration.VersionInfo.BuildVersion)) - { - newCommand.Headers.Add(new KeyValuePair("X-Parse-App-Build-Version", configuration.VersionInfo.BuildVersion)); - } - if (!String.IsNullOrEmpty(configuration.VersionInfo.DisplayVersion)) - { - newCommand.Headers.Add(new KeyValuePair("X-Parse-App-Display-Version", configuration.VersionInfo.DisplayVersion)); - } - if (!String.IsNullOrEmpty(configuration.VersionInfo.OSVersion)) - { - newCommand.Headers.Add(new KeyValuePair("X-Parse-OS-Version", configuration.VersionInfo.OSVersion)); - } - - // TODO (richardross): I hate the idea of having this super tightly coupled static variable in here. - // Lets eventually get rid of it. - if (!String.IsNullOrEmpty(ParseClient.MasterKey)) - { - newCommand.Headers.Add(new KeyValuePair("X-Parse-Master-Key", ParseClient.MasterKey)); - } - else - { - newCommand.Headers.Add(new KeyValuePair("X-Parse-Windows-Key", configuration.Key)); - } - - // TODO (richardross): Inject this instead of using static here. - if (ParseUser.IsRevocableSessionEnabled) - { - newCommand.Headers.Add(new KeyValuePair("X-Parse-Revocable-Session", revocableSessionTokentrueValue)); - } - - return installationIdTask; - } - } -} diff --git a/Parse/Internal/Config/Controller/ParseConfigController.cs b/Parse/Internal/Config/Controller/ParseConfigController.cs deleted file mode 100644 index 44018fed..00000000 --- a/Parse/Internal/Config/Controller/ParseConfigController.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Threading.Tasks; -using System.Threading; -using Parse.Common.Internal; - -namespace Parse.Core.Internal -{ - /// - /// Config controller. - /// - internal class ParseConfigController : IParseConfigController - { - private readonly IParseCommandRunner commandRunner; - - /// - /// Initializes a new instance of the class. - /// - public ParseConfigController(IParseCommandRunner commandRunner, IStorageController storageController) - { - this.commandRunner = commandRunner; - CurrentConfigController = new ParseCurrentConfigController(storageController); - } - - public IParseCommandRunner CommandRunner { get; internal set; } - public IParseCurrentConfigController CurrentConfigController { get; internal set; } - - public Task FetchConfigAsync(string sessionToken, CancellationToken cancellationToken) - { - var command = new ParseCommand("config", - method: "GET", - sessionToken: sessionToken, - data: null); - - return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(task => - { - cancellationToken.ThrowIfCancellationRequested(); - return new ParseConfig(task.Result.Item2); - }).OnSuccess(task => - { - cancellationToken.ThrowIfCancellationRequested(); - CurrentConfigController.SetCurrentConfigAsync(task.Result); - return task; - }).Unwrap(); - } - } -} diff --git a/Parse/Internal/Config/Controller/ParseCurrentConfigController.cs b/Parse/Internal/Config/Controller/ParseCurrentConfigController.cs deleted file mode 100644 index c0d432ed..00000000 --- a/Parse/Internal/Config/Controller/ParseCurrentConfigController.cs +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Threading.Tasks; -using System.Threading; -using System.Collections.Generic; -using Parse.Common.Internal; - -namespace Parse.Core.Internal -{ - /// - /// Parse current config controller. - /// - internal class ParseCurrentConfigController : IParseCurrentConfigController - { - private const string CurrentConfigKey = "CurrentConfig"; - - private readonly TaskQueue taskQueue; - private ParseConfig currentConfig; - - private IStorageController storageController; - - /// - /// Initializes a new instance of the class. - /// - public ParseCurrentConfigController(IStorageController storageController) - { - this.storageController = storageController; - - taskQueue = new TaskQueue(); - } - - public Task GetCurrentConfigAsync() - { - return taskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => - { - if (currentConfig == null) - { - return storageController.LoadAsync().OnSuccess(t => - { - object tmp; - t.Result.TryGetValue(CurrentConfigKey, out tmp); - - string propertiesString = tmp as string; - if (propertiesString != null) - { - var dictionary = ParseClient.DeserializeJsonString(propertiesString); - currentConfig = new ParseConfig(dictionary); - } - else - { - currentConfig = new ParseConfig(); - } - - return currentConfig; - }); - } - - return Task.FromResult(currentConfig); - }), CancellationToken.None).Unwrap(); - } - - public Task SetCurrentConfigAsync(ParseConfig config) - { - return taskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => - { - currentConfig = config; - - var jsonObject = ((IJsonConvertible) config).ToJSON(); - var jsonString = ParseClient.SerializeJsonString(jsonObject); - - return storageController.LoadAsync().OnSuccess(t => t.Result.AddAsync(CurrentConfigKey, jsonString)); - }).Unwrap().Unwrap(), CancellationToken.None); - } - - public Task ClearCurrentConfigAsync() - { - return taskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => - { - currentConfig = null; - - return storageController.LoadAsync().OnSuccess(t => t.Result.RemoveAsync(CurrentConfigKey)); - }).Unwrap().Unwrap(), CancellationToken.None); - } - - public Task ClearCurrentConfigInMemoryAsync() - { - return taskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => - { - currentConfig = null; - }), CancellationToken.None); - } - } -} diff --git a/Parse/Internal/Encoding/NoObjectsEncoder.cs b/Parse/Internal/Encoding/NoObjectsEncoder.cs deleted file mode 100644 index 92d1662e..00000000 --- a/Parse/Internal/Encoding/NoObjectsEncoder.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Collections.Generic; - -namespace Parse.Core.Internal -{ - /// - /// A that throws an exception if it attempts to encode - /// a - /// - public class NoObjectsEncoder : ParseEncoder - { - // This class isn't really a Singleton, but since it has no state, it's more efficient to get - // the default instance. - private static readonly NoObjectsEncoder instance = new NoObjectsEncoder(); - public static NoObjectsEncoder Instance - { - get - { - return instance; - } - } - - protected override IDictionary EncodeParseObject(ParseObject value) - { - throw new ArgumentException("ParseObjects not allowed here."); - } - } -} diff --git a/Parse/Internal/Encoding/ParseDecoder.cs b/Parse/Internal/Encoding/ParseDecoder.cs deleted file mode 100644 index 9128e15a..00000000 --- a/Parse/Internal/Encoding/ParseDecoder.cs +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Linq; -using System.Collections.Generic; -using System.Globalization; -using Parse.Utilities; - -namespace Parse.Core.Internal -{ - public class ParseDecoder - { - // This class isn't really a Singleton, but since it has no state, it's more efficient to get the default instance. - public static ParseDecoder Instance { get; } = new ParseDecoder(); - - // Prevent default constructor. - private ParseDecoder() { } - - public object Decode(object data) - { - if (data == null) - { - return null; - } - - var dict = data as IDictionary; - if (dict != null) - { - if (dict.ContainsKey("__op")) - { - return ParseFieldOperations.Decode(dict); - } - - object type; - dict.TryGetValue("__type", out type); - var typeString = type as string; - - if (typeString == null) - { - var newDict = new Dictionary(); - foreach (var pair in dict) - { - newDict[pair.Key] = Decode(pair.Value); - } - return newDict; - } - - if (typeString == "Date") - { - return ParseDate(dict["iso"] as string); - } - - if (typeString == "Bytes") - { - return Convert.FromBase64String(dict["base64"] as string); - } - - if (typeString == "Pointer") - { - return DecodePointer(dict["className"] as string, dict["objectId"] as string); - } - - if (typeString == "File") - { - return new ParseFile(dict["name"] as string, new Uri(dict["url"] as string)); - } - - if (typeString == "GeoPoint") - { - return new ParseGeoPoint(Conversion.To(dict["latitude"]), - Conversion.To(dict["longitude"])); - } - - if (typeString == "Object") - { - var state = ParseObjectCoder.Instance.Decode(dict, this); - return ParseObject.FromState(state, dict["className"] as string); - } - - if (typeString == "Relation") - { - return ParseRelationBase.CreateRelation(null, null, dict["className"] as string); - } - - var converted = new Dictionary(); - foreach (var pair in dict) - { - converted[pair.Key] = Decode(pair.Value); - } - return converted; - } - - var list = data as IList; - if (list != null) - { - return (from item in list - select Decode(item)).ToList(); - } - - return data; - } - - protected virtual object DecodePointer(string className, string objectId) - { - return ParseObject.CreateWithoutData(className, objectId); - } - - public static DateTime ParseDate(string input) - { - // TODO(hallucinogen): Figure out if we should be more flexible with the date formats - // we accept. - return System.DateTime.ParseExact(input, ParseClient.DateFormatStrings, CultureInfo.InvariantCulture, DateTimeStyles.None); - } - } -} diff --git a/Parse/Internal/Encoding/ParseEncoder.cs b/Parse/Internal/Encoding/ParseEncoder.cs deleted file mode 100644 index e414de20..00000000 --- a/Parse/Internal/Encoding/ParseEncoder.cs +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using Parse.Utilities; -using Parse.Common.Internal; - -namespace Parse.Core.Internal -{ - /// - /// A ParseEncoder can be used to transform objects such as into JSON - /// data structures. - /// - /// - public abstract class ParseEncoder - { -#if UNITY - private static readonly bool isCompiledByIL2CPP = AppDomain.CurrentDomain.FriendlyName.Equals("IL2CPP Root Domain"); -#else - private static readonly bool isCompiledByIL2CPP = false; -#endif - - public static bool IsValidType(object value) - { - return value == null || - ReflectionHelpers.IsPrimitive(value.GetType()) || - value is string || - value is ParseObject || - value is ParseACL || - value is ParseFile || - value is ParseGeoPoint || - value is ParseRelationBase || - value is DateTime || - value is byte[] || - Conversion.As>(value) != null || - Conversion.As>(value) != null; - } - - public object Encode(object value) - { - // If this object has a special encoding, encode it and return the - // encoded object. Otherwise, just return the original object. - if (value is DateTime) - { - return new Dictionary { - {"iso", ((DateTime)value).ToString(ParseClient.DateFormatStrings.First(), CultureInfo.InvariantCulture)}, - {"__type", "Date"} - }; - } - - var bytes = value as byte[]; - if (bytes != null) - { - return new Dictionary { - {"__type", "Bytes"}, - {"base64", Convert.ToBase64String(bytes)} - }; - } - - var obj = value as ParseObject; - if (obj != null) - { - return EncodeParseObject(obj); - } - - var jsonConvertible = value as IJsonConvertible; - if (jsonConvertible != null) - { - return jsonConvertible.ToJSON(); - } - - var dict = Conversion.As>(value); - if (dict != null) - { - var json = new Dictionary(); - foreach (var pair in dict) - { - json[pair.Key] = Encode(pair.Value); - } - return json; - } - - var list = Conversion.As>(value); - if (list != null) - { - return EncodeList(list); - } - - // TODO (hallucinogen): convert IParseFieldOperation to IJsonConvertible - var operation = value as IParseFieldOperation; - if (operation != null) - { - return operation.Encode(); - } - - return value; - } - - protected abstract IDictionary EncodeParseObject(ParseObject value); - - private object EncodeList(IList list) - { - var newArray = new List(); - // We need to explicitly cast `list` to `List` rather than - // `IList` because IL2CPP is stricter than the usual Unity AOT compiler pipeline. - if (isCompiledByIL2CPP && list.GetType().IsArray) - { - list = new List(list); - } - foreach (var item in list) - { - if (!IsValidType(item)) - { - throw new ArgumentException("Invalid type for value in an array"); - } - newArray.Add(Encode(item)); - } - return newArray; - } - } -} diff --git a/Parse/Internal/Encoding/PointerOrLocalIdEncoder.cs b/Parse/Internal/Encoding/PointerOrLocalIdEncoder.cs deleted file mode 100644 index d6386203..00000000 --- a/Parse/Internal/Encoding/PointerOrLocalIdEncoder.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Collections.Generic; - -namespace Parse.Core.Internal -{ - /// - /// A that encode as pointers. If the object - /// does not have an , uses a local id. - /// - public class PointerOrLocalIdEncoder : ParseEncoder - { - // This class isn't really a Singleton, but since it has no state, it's more efficient to get - // the default instance. - private static readonly PointerOrLocalIdEncoder instance = new PointerOrLocalIdEncoder(); - public static PointerOrLocalIdEncoder Instance - { - get - { - return instance; - } - } - - protected override IDictionary EncodeParseObject(ParseObject value) - { - if (value.ObjectId == null) - { - // TODO (hallucinogen): handle local id. For now we throw. - throw new ArgumentException("Cannot create a pointer to an object without an objectId"); - } - - return new Dictionary { - {"__type", "Pointer"}, - {"className", value.ClassName}, - {"objectId", value.ObjectId} - }; - } - } -} diff --git a/Parse/Internal/File/Controller/ParseFileController.cs b/Parse/Internal/File/Controller/ParseFileController.cs deleted file mode 100644 index 48bebd7e..00000000 --- a/Parse/Internal/File/Controller/ParseFileController.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Parse.Common.Internal; - -namespace Parse.Core.Internal -{ - public class ParseFileController : IParseFileController - { - private readonly IParseCommandRunner commandRunner; - - public ParseFileController(IParseCommandRunner commandRunner) - { - this.commandRunner = commandRunner; - } - - public Task SaveAsync(FileState state, - Stream dataStream, - string sessionToken, - IProgress progress, - CancellationToken cancellationToken = default(CancellationToken)) - { - if (state.Url != null) - { - // !isDirty - return Task.FromResult(state); - } - - if (cancellationToken.IsCancellationRequested) - { - var tcs = new TaskCompletionSource(); - tcs.TrySetCanceled(); - return tcs.Task; - } - - var oldPosition = dataStream.Position; - var command = new ParseCommand("files/" + state.Name, - method: "POST", - sessionToken: sessionToken, - contentType: state.MimeType, - stream: dataStream); - - return commandRunner.RunCommandAsync(command, - uploadProgress: progress, - cancellationToken: cancellationToken).OnSuccess(uploadTask => - { - var result = uploadTask.Result; - var jsonData = result.Item2; - cancellationToken.ThrowIfCancellationRequested(); - - return new FileState - { - Name = jsonData["name"] as string, - Url = new Uri(jsonData["url"] as string, UriKind.Absolute), - MimeType = state.MimeType - }; - }).ContinueWith(t => - { - // Rewind the stream on failure or cancellation (if possible) - if ((t.IsFaulted || t.IsCanceled) && dataStream.CanSeek) - { - dataStream.Seek(oldPosition, SeekOrigin.Begin); - } - return t; - }).Unwrap(); - } - } -} diff --git a/Parse/Internal/File/State/FileState.cs b/Parse/Internal/File/State/FileState.cs deleted file mode 100644 index 3c0403df..00000000 --- a/Parse/Internal/File/State/FileState.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; - -namespace Parse.Core.Internal -{ - public class FileState - { - private const string ParseFileSecureScheme = "https"; - private const string ParseFileSecureDomain = "files.parsetfss.com"; - - public string Name { get; set; } - public string MimeType { get; set; } - public Uri Url { get; set; } - public Uri SecureUrl - { - get - { - Uri uri = Url; - if (uri != null && uri.Host == ParseFileSecureDomain) - { - return new UriBuilder(uri) - { - Scheme = ParseFileSecureScheme, - Port = -1, // This makes URIBuilder assign the default port for the URL scheme. - }.Uri; - } - return uri; - } - } - } -} diff --git a/Parse/Internal/IParseCorePlugins.cs b/Parse/Internal/IParseCorePlugins.cs deleted file mode 100644 index 73d8dc76..00000000 --- a/Parse/Internal/IParseCorePlugins.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using Parse.Common.Internal; -using System; - -namespace Parse.Core.Internal -{ - public interface IParseCorePlugins - { - void Reset(); - - IHttpClient HttpClient { get; } - IParseCommandRunner CommandRunner { get; } - IStorageController StorageController { get; } - - IParseCloudCodeController CloudCodeController { get; } - IParseConfigController ConfigController { get; } - IParseFileController FileController { get; } - IParseObjectController ObjectController { get; } - IParseQueryController QueryController { get; } - IParseSessionController SessionController { get; } - IParseUserController UserController { get; } - IObjectSubclassingController SubclassingController { get; } - IParseCurrentUserController CurrentUserController { get; } - IInstallationIdController InstallationIdController { get; } - } -} \ No newline at end of file diff --git a/Parse/Internal/InstallationId/Controller/InstallationIdController.cs b/Parse/Internal/InstallationId/Controller/InstallationIdController.cs deleted file mode 100644 index 8e033c0d..00000000 --- a/Parse/Internal/InstallationId/Controller/InstallationIdController.cs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using Parse.Common.Internal; -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Parse.Core.Internal -{ - public class InstallationIdController : IInstallationIdController - { - private const string InstallationIdKey = "InstallationId"; - private readonly object mutex = new object(); - private Guid? installationId; - - private readonly IStorageController storageController; - public InstallationIdController(IStorageController storageController) - { - this.storageController = storageController; - } - - public Task SetAsync(Guid? installationId) - { - lock (mutex) - { - Task saveTask; - - if (installationId == null) - { - saveTask = storageController - .LoadAsync() - .OnSuccess(storage => storage.Result.RemoveAsync(InstallationIdKey)) - .Unwrap(); - } - else - { - saveTask = storageController - .LoadAsync() - .OnSuccess(storage => storage.Result.AddAsync(InstallationIdKey, installationId.ToString())) - .Unwrap(); - } - this.installationId = installationId; - return saveTask; - } - } - - public Task GetAsync() - { - lock (mutex) - { - if (installationId != null) - { - return Task.FromResult(installationId); - } - } - - return storageController - .LoadAsync() - .OnSuccess, Task>(s => - { - object id; - s.Result.TryGetValue(InstallationIdKey, out id); - try - { - lock (mutex) - { - installationId = new Guid((string) id); - return Task.FromResult(installationId); - } - } - catch (Exception) - { - var newInstallationId = Guid.NewGuid(); - return SetAsync(newInstallationId).OnSuccess(_ => newInstallationId); - } - }) - .Unwrap(); - } - - public Task ClearAsync() - { - return SetAsync(null); - } - } -} diff --git a/Parse/Internal/Modules/IParseModule.cs b/Parse/Internal/Modules/IParseModule.cs deleted file mode 100644 index 00be08d5..00000000 --- a/Parse/Internal/Modules/IParseModule.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Parse.Common.Internal -{ - public interface IParseModule - { - void OnModuleRegistered(); - void OnParseInitialized(); - } -} \ No newline at end of file diff --git a/Parse/Internal/Modules/ParseModuleAttribute.cs b/Parse/Internal/Modules/ParseModuleAttribute.cs deleted file mode 100644 index 0a9c93d6..00000000 --- a/Parse/Internal/Modules/ParseModuleAttribute.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace Parse.Common.Internal -{ - [AttributeUsage(AttributeTargets.Assembly)] - public class ParseModuleAttribute : Attribute - { - /// - /// Instantiates a new ParseModuleAttribute. - /// - /// The type to which this module is applied. - public ParseModuleAttribute(Type ModuleType) - { - this.ModuleType = ModuleType; - } - - public Type ModuleType { get; private set; } - } -} \ No newline at end of file diff --git a/Parse/Internal/Modules/ParseModuleController.cs b/Parse/Internal/Modules/ParseModuleController.cs deleted file mode 100644 index 841c7403..00000000 --- a/Parse/Internal/Modules/ParseModuleController.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using AssemblyLister; - -namespace Parse.Common.Internal -{ - /// - /// The class which controls the loading of other ParseModules - /// - public class ParseModuleController - { - private static readonly ParseModuleController instance = new ParseModuleController(); - public static ParseModuleController Instance - { - get { return instance; } - } - - private readonly object mutex = new object(); - private readonly List modules = new List(); - - private bool isParseInitialized = false; - - public void RegisterModule(IParseModule module) - { - if (module == null) - { - return; - } - - lock (mutex) - { - modules.Add(module); - module.OnModuleRegistered(); - - if (isParseInitialized) - { - module.OnParseInitialized(); - } - } - } - - public void ScanForModules() - { - var moduleTypes = Lister.AllAssemblies - .SelectMany(asm => asm.GetCustomAttributes()) - .Select(attr => attr.ModuleType) - .Where(type => type != null && type.GetTypeInfo().ImplementedInterfaces.Contains(typeof(IParseModule))); - - lock (mutex) - { - foreach (Type moduleType in moduleTypes) - { - try - { - ConstructorInfo constructor = moduleType.FindConstructor(); - if (constructor != null) - { - var module = constructor.Invoke(new object[] { }) as IParseModule; - RegisterModule(module); - } - } - catch (Exception) - { - // Ignore, either constructor threw or was private. - } - } - } - } - - public void Reset() - { - lock (mutex) - { - modules.Clear(); - isParseInitialized = false; - } - } - - public void ParseDidInitialize() - { - lock (mutex) - { - foreach (IParseModule module in modules) - { - module.OnParseInitialized(); - } - isParseInitialized = true; - } - } - } -} \ No newline at end of file diff --git a/Parse/Internal/Object/Controller/ParseObjectController.cs b/Parse/Internal/Object/Controller/ParseObjectController.cs deleted file mode 100644 index 40356da3..00000000 --- a/Parse/Internal/Object/Controller/ParseObjectController.cs +++ /dev/null @@ -1,231 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Linq; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Parse.Utilities; -using Parse.Common.Internal; - -namespace Parse.Core.Internal -{ - public class ParseObjectController : IParseObjectController - { - private readonly IParseCommandRunner commandRunner; - - public ParseObjectController(IParseCommandRunner commandRunner) - { - this.commandRunner = commandRunner; - } - - public Task FetchAsync(IObjectState state, - string sessionToken, - CancellationToken cancellationToken) - { - var command = new ParseCommand(string.Format("classes/{0}/{1}", - Uri.EscapeDataString(state.ClassName), - Uri.EscapeDataString(state.ObjectId)), - method: "GET", - sessionToken: sessionToken, - data: null); - - return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => - { - return ParseObjectCoder.Instance.Decode(t.Result.Item2, ParseDecoder.Instance); - }); - } - - public Task SaveAsync(IObjectState state, - IDictionary operations, - string sessionToken, - CancellationToken cancellationToken) - { - var objectJSON = ParseObject.ToJSONObjectForSaving(operations); - - var command = new ParseCommand((state.ObjectId == null ? - string.Format("classes/{0}", Uri.EscapeDataString(state.ClassName)) : - string.Format("classes/{0}/{1}", Uri.EscapeDataString(state.ClassName), state.ObjectId)), - method: (state.ObjectId == null ? "POST" : "PUT"), - sessionToken: sessionToken, - data: objectJSON); - - return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => - { - var serverState = ParseObjectCoder.Instance.Decode(t.Result.Item2, ParseDecoder.Instance); - serverState = serverState.MutatedClone(mutableClone => - { - mutableClone.IsNew = t.Result.Item1 == System.Net.HttpStatusCode.Created; - }); - return serverState; - }); - } - - public IList> SaveAllAsync(IList states, - IList> operationsList, - string sessionToken, - CancellationToken cancellationToken) - { - - var requests = states - .Zip(operationsList, (item, ops) => new ParseCommand( - item.ObjectId == null - ? string.Format("classes/{0}", Uri.EscapeDataString(item.ClassName)) - : string.Format("classes/{0}/{1}", Uri.EscapeDataString(item.ClassName), Uri.EscapeDataString(item.ObjectId)), - method: item.ObjectId == null ? "POST" : "PUT", - data: ParseObject.ToJSONObjectForSaving(ops))) - .ToList(); - - var batchTasks = ExecuteBatchRequests(requests, sessionToken, cancellationToken); - var stateTasks = new List>(); - foreach (var task in batchTasks) - { - stateTasks.Add(task.OnSuccess(t => - { - return ParseObjectCoder.Instance.Decode(t.Result, ParseDecoder.Instance); - })); - } - - return stateTasks; - } - - public Task DeleteAsync(IObjectState state, - string sessionToken, - CancellationToken cancellationToken) - { - var command = new ParseCommand(string.Format("classes/{0}/{1}", - state.ClassName, state.ObjectId), - method: "DELETE", - sessionToken: sessionToken, - data: null); - - return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken); - } - - public IList DeleteAllAsync(IList states, - string sessionToken, - CancellationToken cancellationToken) - { - var requests = states - .Where(item => item.ObjectId != null) - .Select(item => new ParseCommand( - string.Format("classes/{0}/{1}", Uri.EscapeDataString(item.ClassName), Uri.EscapeDataString(item.ObjectId)), - method: "DELETE", - data: null)) - .ToList(); - return ExecuteBatchRequests(requests, sessionToken, cancellationToken).Cast().ToList(); - } - - // TODO (hallucinogen): move this out to a class to be used by Analytics - private const int MaximumBatchSize = 50; - internal IList>> ExecuteBatchRequests(IList requests, - string sessionToken, - CancellationToken cancellationToken) - { - var tasks = new List>>(); - int batchSize = requests.Count; - - IEnumerable remaining = requests; - while (batchSize > MaximumBatchSize) - { - var process = remaining.Take(MaximumBatchSize).ToList(); - remaining = remaining.Skip(MaximumBatchSize); - - tasks.AddRange(ExecuteBatchRequest(process, sessionToken, cancellationToken)); - - batchSize = remaining.Count(); - } - tasks.AddRange(ExecuteBatchRequest(remaining.ToList(), sessionToken, cancellationToken)); - - return tasks; - } - - private IList>> ExecuteBatchRequest(IList requests, - string sessionToken, - CancellationToken cancellationToken) - { - var tasks = new List>>(); - int batchSize = requests.Count; - var tcss = new List>>(); - for (int i = 0; i < batchSize; ++i) - { - var tcs = new TaskCompletionSource>(); - tcss.Add(tcs); - tasks.Add(tcs.Task); - } - - var encodedRequests = requests.Select(r => - { - var results = new Dictionary { - { "method", r.Method }, - { "path", r.Uri.AbsolutePath }, - }; - - if (r.DataObject != null) - { - results["body"] = r.DataObject; - } - return results; - }).Cast().ToList(); - var command = new ParseCommand("batch", - method: "POST", - sessionToken: sessionToken, - data: new Dictionary { { "requests", encodedRequests } }); - - commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).ContinueWith(t => - { - if (t.IsFaulted || t.IsCanceled) - { - foreach (var tcs in tcss) - { - if (t.IsFaulted) - { - tcs.TrySetException(t.Exception); - } - else if (t.IsCanceled) - { - tcs.TrySetCanceled(); - } - } - return; - } - - var resultsArray = Conversion.As>(t.Result.Item2["results"]); - int resultLength = resultsArray.Count; - if (resultLength != batchSize) - { - foreach (var tcs in tcss) - { - tcs.TrySetException(new InvalidOperationException( - "Batch command result count expected: " + batchSize + " but was: " + resultLength + ".")); - } - return; - } - - for (int i = 0; i < batchSize; ++i) - { - var result = resultsArray[i] as Dictionary; - var tcs = tcss[i]; - - if (result.ContainsKey("success")) - { - tcs.TrySetResult(result["success"] as IDictionary); - } - else if (result.ContainsKey("error")) - { - var error = result["error"] as IDictionary; - long errorCode = (long) error["code"]; - tcs.TrySetException(new ParseException((ParseException.ErrorCode) errorCode, error["error"] as string)); - } - else - { - tcs.TrySetException(new InvalidOperationException( - "Invalid batch command response.")); - } - } - }); - - return tasks; - } - } -} diff --git a/Parse/Internal/Object/State/MutableObjectState.cs b/Parse/Internal/Object/State/MutableObjectState.cs deleted file mode 100644 index 4a771803..00000000 --- a/Parse/Internal/Object/State/MutableObjectState.cs +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Linq; -using System.Collections.Generic; - -namespace Parse.Core.Internal -{ - public class MutableObjectState : IObjectState - { - public bool IsNew { get; set; } - public string ClassName { get; set; } - public string ObjectId { get; set; } - public DateTime? UpdatedAt { get; set; } - public DateTime? CreatedAt { get; set; } - - // Initialize serverData to avoid further null checking. - private IDictionary serverData = new Dictionary(); - public IDictionary ServerData - { - get - { - return serverData; - } - - set - { - serverData = value; - } - } - - public object this[string key] - { - get - { - return ServerData[key]; - } - } - - public bool ContainsKey(string key) - { - return ServerData.ContainsKey(key); - } - - public void Apply(IDictionary operationSet) - { - // Apply operationSet - foreach (var pair in operationSet) - { - object oldValue; - ServerData.TryGetValue(pair.Key, out oldValue); - var newValue = pair.Value.Apply(oldValue, pair.Key); - if (newValue != ParseDeleteOperation.DeleteToken) - { - ServerData[pair.Key] = newValue; - } - else - { - ServerData.Remove(pair.Key); - } - } - } - - public void Apply(IObjectState other) - { - IsNew = other.IsNew; - if (other.ObjectId != null) - { - ObjectId = other.ObjectId; - } - if (other.UpdatedAt != null) - { - UpdatedAt = other.UpdatedAt; - } - if (other.CreatedAt != null) - { - CreatedAt = other.CreatedAt; - } - - foreach (var pair in other) - { - ServerData[pair.Key] = pair.Value; - } - } - - public IObjectState MutatedClone(Action func) - { - var clone = MutableClone(); - func(clone); - return clone; - } - - protected virtual MutableObjectState MutableClone() - { - return new MutableObjectState - { - IsNew = IsNew, - ClassName = ClassName, - ObjectId = ObjectId, - CreatedAt = CreatedAt, - UpdatedAt = UpdatedAt, - ServerData = this.ToDictionary(t => t.Key, t => t.Value) - }; - } - - IEnumerator> IEnumerable>.GetEnumerator() - { - return ServerData.GetEnumerator(); - } - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() - { - return ((IEnumerable>) this).GetEnumerator(); - } - } -} diff --git a/Parse/Internal/Object/Subclassing/IObjectSubclassingController.cs b/Parse/Internal/Object/Subclassing/IObjectSubclassingController.cs deleted file mode 100644 index ab4932d0..00000000 --- a/Parse/Internal/Object/Subclassing/IObjectSubclassingController.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Parse.Core.Internal -{ - public interface IObjectSubclassingController - { - string GetClassName(Type type); - Type GetType(string className); - - bool IsTypeValid(string className, Type type); - - void RegisterSubclass(Type t); - void UnregisterSubclass(Type t); - - void AddRegisterHook(Type t, Action action); - - ParseObject Instantiate(string className); - IDictionary GetPropertyMappings(string className); - } -} diff --git a/Parse/Internal/Object/Subclassing/ObjectSubclassInfo.cs b/Parse/Internal/Object/Subclassing/ObjectSubclassInfo.cs deleted file mode 100644 index e117ece7..00000000 --- a/Parse/Internal/Object/Subclassing/ObjectSubclassInfo.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Reflection; -using Parse.Common.Internal; - -namespace Parse.Core.Internal -{ - internal class ObjectSubclassInfo - { - public ObjectSubclassInfo(Type type, ConstructorInfo constructor) - { - TypeInfo = type.GetTypeInfo(); - ClassName = GetClassName(TypeInfo); - Constructor = constructor; - PropertyMappings = ReflectionHelpers.GetProperties(type).Select(prop => Tuple.Create(prop, prop.GetCustomAttribute(true))).Where(t => t.Item2 != null).Select(t => Tuple.Create(t.Item1, t.Item2.FieldName)).ToDictionary(t => t.Item1.Name, t => t.Item2); - } - - public TypeInfo TypeInfo { get; private set; } - public string ClassName { get; private set; } - public IDictionary PropertyMappings { get; private set; } - private ConstructorInfo Constructor { get; set; } - - public ParseObject Instantiate() => (ParseObject) Constructor.Invoke(null); - - internal static string GetClassName(TypeInfo type) => type.GetCustomAttribute()?.ClassName; - } -} diff --git a/Parse/Internal/Object/Subclassing/ObjectSubclassingController.cs b/Parse/Internal/Object/Subclassing/ObjectSubclassingController.cs deleted file mode 100644 index af1047d0..00000000 --- a/Parse/Internal/Object/Subclassing/ObjectSubclassingController.cs +++ /dev/null @@ -1,156 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Threading; -using Parse.Common.Internal; - -namespace Parse.Core.Internal -{ - internal class ObjectSubclassingController : IObjectSubclassingController - { - // Class names starting with _ are documented to be reserved. Use this one - // here to allow us to 'inherit' certain properties. - private static readonly string parseObjectClassName = "_ParseObject"; - - private readonly ReaderWriterLockSlim mutex; - private readonly IDictionary registeredSubclasses; - private Dictionary registerActions; - - public ObjectSubclassingController() - { - mutex = new ReaderWriterLockSlim(); - registeredSubclasses = new Dictionary(); - registerActions = new Dictionary(); - - // Register the ParseObject subclass, so we get access to the ACL, - // objectId, and other ParseFieldName properties. - RegisterSubclass(typeof(ParseObject)); - } - - public string GetClassName(Type type) => type == typeof(ParseObject) ? parseObjectClassName : ObjectSubclassInfo.GetClassName(type.GetTypeInfo()); - - public Type GetType(string className) - { - mutex.EnterReadLock(); - registeredSubclasses.TryGetValue(className, out ObjectSubclassInfo info); - mutex.ExitReadLock(); - - return info?.TypeInfo.AsType(); - } - - public bool IsTypeValid(string className, Type type) - { - mutex.EnterReadLock(); - registeredSubclasses.TryGetValue(className, out ObjectSubclassInfo subclassInfo); - mutex.ExitReadLock(); - - return subclassInfo == null ? type == typeof(ParseObject) : subclassInfo.TypeInfo == type.GetTypeInfo(); - } - - public void RegisterSubclass(Type type) - { - TypeInfo typeInfo = type.GetTypeInfo(); - if (!typeof(ParseObject).GetTypeInfo().IsAssignableFrom(typeInfo)) - { - throw new ArgumentException("Cannot register a type that is not a subclass of ParseObject"); - } - - string className = GetClassName(type); - - try - { - // Perform this as a single independent transaction, so we can never get into an - // intermediate state where we *theoretically* register the wrong class due to a - // TOCTTOU bug. - mutex.EnterWriteLock(); - - ObjectSubclassInfo previousInfo = null; - if (registeredSubclasses.TryGetValue(className, out previousInfo)) - { - if (typeInfo.IsAssignableFrom(previousInfo.TypeInfo)) - { - // Previous subclass is more specific or equal to the current type, do nothing. - return; - } - else if (previousInfo.TypeInfo.IsAssignableFrom(typeInfo)) - { - // Previous subclass is parent of new child, fallthrough and actually register - // this class. - /* Do nothing */ - } - else - { - throw new ArgumentException( - "Tried to register both " + previousInfo.TypeInfo.FullName + " and " + typeInfo.FullName + - " as the ParseObject subclass of " + className + ". Cannot determine the right class " + - "to use because neither inherits from the other." - ); - } - } - - ConstructorInfo constructor = type.FindConstructor(); - if (constructor == null) - { - throw new ArgumentException("Cannot register a type that does not implement the default constructor!"); - } - - registeredSubclasses[className] = new ObjectSubclassInfo(type, constructor); - } - finally - { - mutex.ExitWriteLock(); - } - - Action toPerform; - - mutex.EnterReadLock(); - registerActions.TryGetValue(className, out toPerform); - mutex.ExitReadLock(); - - toPerform?.Invoke(); - } - - public void UnregisterSubclass(Type type) - { - mutex.EnterWriteLock(); - registeredSubclasses.Remove(GetClassName(type)); - mutex.ExitWriteLock(); - } - - public void AddRegisterHook(Type t, Action action) - { - mutex.EnterWriteLock(); - registerActions.Add(GetClassName(t), action); - mutex.ExitWriteLock(); - } - - public ParseObject Instantiate(string className) - { - ObjectSubclassInfo info = null; - - mutex.EnterReadLock(); - registeredSubclasses.TryGetValue(className, out info); - mutex.ExitReadLock(); - - return info != null - ? info.Instantiate() - : new ParseObject(className); - } - - public IDictionary GetPropertyMappings(string className) - { - ObjectSubclassInfo info = null; - mutex.EnterReadLock(); - registeredSubclasses.TryGetValue(className, out info); - if (info == null) - { - registeredSubclasses.TryGetValue(parseObjectClassName, out info); - } - mutex.ExitReadLock(); - - return info.PropertyMappings; - } - - } -} diff --git a/Parse/Internal/Operation/ParseAddOperation.cs b/Parse/Internal/Operation/ParseAddOperation.cs deleted file mode 100644 index efa2ae63..00000000 --- a/Parse/Internal/Operation/ParseAddOperation.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using Parse.Utilities; - -namespace Parse.Core.Internal -{ - public class ParseAddOperation : IParseFieldOperation - { - private ReadOnlyCollection objects; - public ParseAddOperation(IEnumerable objects) - { - this.objects = new ReadOnlyCollection(objects.ToList()); - } - - public object Encode() - { - return new Dictionary { - {"__op", "Add"}, - {"objects", PointerOrLocalIdEncoder.Instance.Encode(objects)} - }; - } - - public IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous) - { - if (previous == null) - { - return this; - } - if (previous is ParseDeleteOperation) - { - return new ParseSetOperation(objects.ToList()); - } - if (previous is ParseSetOperation) - { - var setOp = (ParseSetOperation) previous; - var oldList = Conversion.To>(setOp.Value); - return new ParseSetOperation(oldList.Concat(objects).ToList()); - } - if (previous is ParseAddOperation) - { - return new ParseAddOperation(((ParseAddOperation) previous).Objects.Concat(objects)); - } - throw new InvalidOperationException("Operation is invalid after previous operation."); - } - - public object Apply(object oldValue, string key) - { - if (oldValue == null) - { - return objects.ToList(); - } - var oldList = Conversion.To>(oldValue); - return oldList.Concat(objects).ToList(); - } - - public IEnumerable Objects - { - get - { - return objects; - } - } - } -} diff --git a/Parse/Internal/Operation/ParseAddUniqueOperation.cs b/Parse/Internal/Operation/ParseAddUniqueOperation.cs deleted file mode 100644 index 2e269945..00000000 --- a/Parse/Internal/Operation/ParseAddUniqueOperation.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using Parse.Utilities; - -namespace Parse.Core.Internal -{ - public class ParseAddUniqueOperation : IParseFieldOperation - { - private ReadOnlyCollection objects; - public ParseAddUniqueOperation(IEnumerable objects) - { - this.objects = new ReadOnlyCollection(objects.Distinct().ToList()); - } - - public object Encode() - { - return new Dictionary { - {"__op", "AddUnique"}, - {"objects", PointerOrLocalIdEncoder.Instance.Encode(objects)} - }; - } - - public IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous) - { - if (previous == null) - { - return this; - } - if (previous is ParseDeleteOperation) - { - return new ParseSetOperation(objects.ToList()); - } - if (previous is ParseSetOperation) - { - var setOp = (ParseSetOperation) previous; - var oldList = Conversion.To>(setOp.Value); - var result = this.Apply(oldList, null); - return new ParseSetOperation(result); - } - if (previous is ParseAddUniqueOperation) - { - var oldList = ((ParseAddUniqueOperation) previous).Objects; - return new ParseAddUniqueOperation((IList) this.Apply(oldList, null)); - } - throw new InvalidOperationException("Operation is invalid after previous operation."); - } - - public object Apply(object oldValue, string key) - { - if (oldValue == null) - { - return objects.ToList(); - } - var newList = Conversion.To>(oldValue).ToList(); - var comparer = ParseFieldOperations.ParseObjectComparer; - foreach (var objToAdd in objects) - { - if (objToAdd is ParseObject) - { - var matchedObj = newList.FirstOrDefault(listObj => comparer.Equals(objToAdd, listObj)); - if (matchedObj == null) - { - newList.Add(objToAdd); - } - else - { - var index = newList.IndexOf(matchedObj); - newList[index] = objToAdd; - } - } - else if (!newList.Contains(objToAdd, comparer)) - { - newList.Add(objToAdd); - } - } - return newList; - } - - public IEnumerable Objects - { - get - { - return objects; - } - } - } -} diff --git a/Parse/Internal/Operation/ParseDeleteOperation.cs b/Parse/Internal/Operation/ParseDeleteOperation.cs deleted file mode 100644 index f3d9fa15..00000000 --- a/Parse/Internal/Operation/ParseDeleteOperation.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System.Collections.Generic; - -namespace Parse.Core.Internal -{ - /// - /// An operation where a field is deleted from the object. - /// - public class ParseDeleteOperation : IParseFieldOperation - { - internal static readonly object DeleteToken = new object(); - private static ParseDeleteOperation _Instance = new ParseDeleteOperation(); - public static ParseDeleteOperation Instance - { - get - { - return _Instance; - } - } - - private ParseDeleteOperation() { } - public object Encode() - { - return new Dictionary { - {"__op", "Delete"} - }; - } - - public IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous) - { - return this; - } - - public object Apply(object oldValue, string key) - { - return DeleteToken; - } - } -} diff --git a/Parse/Internal/Operation/ParseIncrementOperation.cs b/Parse/Internal/Operation/ParseIncrementOperation.cs deleted file mode 100644 index 68e59228..00000000 --- a/Parse/Internal/Operation/ParseIncrementOperation.cs +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Parse.Core.Internal -{ - public class ParseIncrementOperation : IParseFieldOperation - { - private static readonly IDictionary, Func> adders; - - static ParseIncrementOperation() - { - // Defines adders for all of the implicit conversions: http://msdn.microsoft.com/en-US/library/y5b434w4(v=vs.80).aspx - adders = new Dictionary, Func> { - {new Tuple(typeof(sbyte), typeof(sbyte)), (left, right) => (sbyte)left + (sbyte)right}, - {new Tuple(typeof(sbyte), typeof(short)), (left, right) => (sbyte)left + (short)right}, - {new Tuple(typeof(sbyte), typeof(int)), (left, right) => (sbyte)left + (int)right}, - {new Tuple(typeof(sbyte), typeof(long)), (left, right) => (sbyte)left + (long)right}, - {new Tuple(typeof(sbyte), typeof(float)), (left, right) => (sbyte)left + (float)right}, - {new Tuple(typeof(sbyte), typeof(double)), (left, right) => (sbyte)left + (double)right}, - {new Tuple(typeof(sbyte), typeof(decimal)), (left, right) => (sbyte)left + (decimal)right}, - {new Tuple(typeof(byte), typeof(byte)), (left, right) => (byte)left + (byte)right}, - {new Tuple(typeof(byte), typeof(short)), (left, right) => (byte)left + (short)right}, - {new Tuple(typeof(byte), typeof(ushort)), (left, right) => (byte)left + (ushort)right}, - {new Tuple(typeof(byte), typeof(int)), (left, right) => (byte)left + (int)right}, - {new Tuple(typeof(byte), typeof(uint)), (left, right) => (byte)left + (uint)right}, - {new Tuple(typeof(byte), typeof(long)), (left, right) => (byte)left + (long)right}, - {new Tuple(typeof(byte), typeof(ulong)), (left, right) => (byte)left + (ulong)right}, - {new Tuple(typeof(byte), typeof(float)), (left, right) => (byte)left + (float)right}, - {new Tuple(typeof(byte), typeof(double)), (left, right) => (byte)left + (double)right}, - {new Tuple(typeof(byte), typeof(decimal)), (left, right) => (byte)left + (decimal)right}, - {new Tuple(typeof(short), typeof(short)), (left, right) => (short)left + (short)right}, - {new Tuple(typeof(short), typeof(int)), (left, right) => (short)left + (int)right}, - {new Tuple(typeof(short), typeof(long)), (left, right) => (short)left + (long)right}, - {new Tuple(typeof(short), typeof(float)), (left, right) => (short)left + (float)right}, - {new Tuple(typeof(short), typeof(double)), (left, right) => (short)left + (double)right}, - {new Tuple(typeof(short), typeof(decimal)), (left, right) => (short)left + (decimal)right}, - {new Tuple(typeof(ushort), typeof(ushort)), (left, right) => (ushort)left + (ushort)right}, - {new Tuple(typeof(ushort), typeof(int)), (left, right) => (ushort)left + (int)right}, - {new Tuple(typeof(ushort), typeof(uint)), (left, right) => (ushort)left + (uint)right}, - {new Tuple(typeof(ushort), typeof(long)), (left, right) => (ushort)left + (long)right}, - {new Tuple(typeof(ushort), typeof(ulong)), (left, right) => (ushort)left + (ulong)right}, - {new Tuple(typeof(ushort), typeof(float)), (left, right) => (ushort)left + (float)right}, - {new Tuple(typeof(ushort), typeof(double)), (left, right) => (ushort)left + (double)right}, - {new Tuple(typeof(ushort), typeof(decimal)), (left, right) => (ushort)left + (decimal)right}, - {new Tuple(typeof(int), typeof(int)), (left, right) => (int)left + (int)right}, - {new Tuple(typeof(int), typeof(long)), (left, right) => (int)left + (long)right}, - {new Tuple(typeof(int), typeof(float)), (left, right) => (int)left + (float)right}, - {new Tuple(typeof(int), typeof(double)), (left, right) => (int)left + (double)right}, - {new Tuple(typeof(int), typeof(decimal)), (left, right) => (int)left + (decimal)right}, - {new Tuple(typeof(uint), typeof(uint)), (left, right) => (uint)left + (uint)right}, - {new Tuple(typeof(uint), typeof(long)), (left, right) => (uint)left + (long)right}, - {new Tuple(typeof(uint), typeof(ulong)), (left, right) => (uint)left + (ulong)right}, - {new Tuple(typeof(uint), typeof(float)), (left, right) => (uint)left + (float)right}, - {new Tuple(typeof(uint), typeof(double)), (left, right) => (uint)left + (double)right}, - {new Tuple(typeof(uint), typeof(decimal)), (left, right) => (uint)left + (decimal)right}, - {new Tuple(typeof(long), typeof(long)), (left, right) => (long)left + (long)right}, - {new Tuple(typeof(long), typeof(float)), (left, right) => (long)left + (float)right}, - {new Tuple(typeof(long), typeof(double)), (left, right) => (long)left + (double)right}, - {new Tuple(typeof(long), typeof(decimal)), (left, right) => (long)left + (decimal)right}, - {new Tuple(typeof(char), typeof(char)), (left, right) => (char)left + (char)right}, - {new Tuple(typeof(char), typeof(ushort)), (left, right) => (char)left + (ushort)right}, - {new Tuple(typeof(char), typeof(int)), (left, right) => (char)left + (int)right}, - {new Tuple(typeof(char), typeof(uint)), (left, right) => (char)left + (uint)right}, - {new Tuple(typeof(char), typeof(long)), (left, right) => (char)left + (long)right}, - {new Tuple(typeof(char), typeof(ulong)), (left, right) => (char)left + (ulong)right}, - {new Tuple(typeof(char), typeof(float)), (left, right) => (char)left + (float)right}, - {new Tuple(typeof(char), typeof(double)), (left, right) => (char)left + (double)right}, - {new Tuple(typeof(char), typeof(decimal)), (left, right) => (char)left + (decimal)right}, - {new Tuple(typeof(float), typeof(float)), (left, right) => (float)left + (float)right}, - {new Tuple(typeof(float), typeof(double)), (left, right) => (float)left + (double)right}, - {new Tuple(typeof(ulong), typeof(ulong)), (left, right) => (ulong)left + (ulong)right}, - {new Tuple(typeof(ulong), typeof(float)), (left, right) => (ulong)left + (float)right}, - {new Tuple(typeof(ulong), typeof(double)), (left, right) => (ulong)left + (double)right}, - {new Tuple(typeof(ulong), typeof(decimal)), (left, right) => (ulong)left + (decimal)right}, - {new Tuple(typeof(double), typeof(double)), (left, right) => (double)left + (double)right}, - {new Tuple(typeof(decimal), typeof(decimal)), (left, right) => (decimal)left + (decimal)right} - }; - // Generate the adders in the other direction - foreach (var pair in adders.Keys.ToList()) - { - if (pair.Item1.Equals(pair.Item2)) - { - continue; - } - var reversePair = new Tuple(pair.Item2, pair.Item1); - var func = adders[pair]; - adders[reversePair] = (left, right) => func(right, left); - } - } - - private object amount; - - public ParseIncrementOperation(object amount) - { - this.amount = amount; - } - - public object Encode() - { - return new Dictionary { - {"__op", "Increment"}, - {"amount", amount} - }; - } - - private static object Add(object obj1, object obj2) - { - Func adder; - if (adders.TryGetValue(new Tuple(obj1.GetType(), obj2.GetType()), out adder)) - { - return adder(obj1, obj2); - } - throw new InvalidCastException("Cannot add " + obj1.GetType() + " to " + obj2.GetType()); - } - - public IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous) - { - if (previous == null) - { - return this; - } - if (previous is ParseDeleteOperation) - { - return new ParseSetOperation(amount); - } - if (previous is ParseSetOperation) - { - var otherAmount = ((ParseSetOperation) previous).Value; - if (otherAmount is string) - { - throw new InvalidOperationException("Cannot increment a non-number type."); - } - var myAmount = amount; - return new ParseSetOperation(Add(otherAmount, myAmount)); - } - if (previous is ParseIncrementOperation) - { - object otherAmount = ((ParseIncrementOperation) previous).Amount; - object myAmount = amount; - return new ParseIncrementOperation(Add(otherAmount, myAmount)); - } - throw new InvalidOperationException("Operation is invalid after previous operation."); - } - - public object Apply(object oldValue, string key) - { - if (oldValue is string) - { - throw new InvalidOperationException("Cannot increment a non-number type."); - } - object otherAmount = oldValue ?? 0; - object myAmount = amount; - return Add(otherAmount, myAmount); - } - - public object Amount - { - get - { - return amount; - } - } - } -} diff --git a/Parse/Internal/Operation/ParseRelationOperation.cs b/Parse/Internal/Operation/ParseRelationOperation.cs deleted file mode 100644 index 4c5a16b6..00000000 --- a/Parse/Internal/Operation/ParseRelationOperation.cs +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Parse.Core.Internal -{ - public class ParseRelationOperation : IParseFieldOperation - { - private readonly IList adds; - private readonly IList removes; - private readonly string targetClassName; - - private ParseRelationOperation(IEnumerable adds, - IEnumerable removes, - string targetClassName) - { - this.targetClassName = targetClassName; - this.adds = new ReadOnlyCollection(adds.ToList()); - this.removes = new ReadOnlyCollection(removes.ToList()); - } - - public ParseRelationOperation(IEnumerable adds, - IEnumerable removes) - { - adds = adds ?? new ParseObject[0]; - removes = removes ?? new ParseObject[0]; - this.targetClassName = adds.Concat(removes).Select(o => o.ClassName).FirstOrDefault(); - this.adds = new ReadOnlyCollection(IdsFromObjects(adds).ToList()); - this.removes = new ReadOnlyCollection(IdsFromObjects(removes).ToList()); - } - - public object Encode() - { - var adds = this.adds - .Select(id => PointerOrLocalIdEncoder.Instance.Encode( - ParseObject.CreateWithoutData(targetClassName, id))) - .ToList(); - var removes = this.removes - .Select(id => PointerOrLocalIdEncoder.Instance.Encode( - ParseObject.CreateWithoutData(targetClassName, id))) - .ToList(); - var addDict = adds.Count == 0 ? null : new Dictionary { - {"__op", "AddRelation"}, - {"objects", adds} - }; - var removeDict = removes.Count == 0 ? null : new Dictionary { - {"__op", "RemoveRelation"}, - {"objects", removes} - }; - - if (addDict != null && removeDict != null) - { - return new Dictionary { - {"__op", "Batch"}, - {"ops", new[] {addDict, removeDict}} - }; - } - return addDict ?? removeDict; - } - - public IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous) - { - if (previous == null) - { - return this; - } - if (previous is ParseDeleteOperation) - { - throw new InvalidOperationException("You can't modify a relation after deleting it."); - } - var other = previous as ParseRelationOperation; - if (other != null) - { - if (other.TargetClassName != TargetClassName) - { - throw new InvalidOperationException( - string.Format("Related object must be of class {0}, but {1} was passed in.", - other.TargetClassName, - TargetClassName)); - } - var newAdd = adds.Union(other.adds.Except(removes)).ToList(); - var newRemove = removes.Union(other.removes.Except(adds)).ToList(); - return new ParseRelationOperation(newAdd, newRemove, TargetClassName); - } - throw new InvalidOperationException("Operation is invalid after previous operation."); - } - - public object Apply(object oldValue, string key) - { - if (adds.Count == 0 && removes.Count == 0) - { - return null; - } - if (oldValue == null) - { - return ParseRelationBase.CreateRelation(null, key, targetClassName); - } - if (oldValue is ParseRelationBase) - { - var oldRelation = (ParseRelationBase) oldValue; - var oldClassName = oldRelation.TargetClassName; - if (oldClassName != null && oldClassName != targetClassName) - { - throw new InvalidOperationException("Related object must be a " + oldClassName - + ", but a " + targetClassName + " was passed in."); - } - oldRelation.TargetClassName = targetClassName; - return oldRelation; - } - throw new InvalidOperationException("Operation is invalid after previous operation."); - } - - public string TargetClassName { get { return targetClassName; } } - - private IEnumerable IdsFromObjects(IEnumerable objects) - { - foreach (var obj in objects) - { - if (obj.ObjectId == null) - { - throw new ArgumentException( - "You can't add an unsaved ParseObject to a relation."); - } - if (obj.ClassName != targetClassName) - { - throw new ArgumentException(string.Format( - "Tried to create a ParseRelation with 2 different types: {0} and {1}", - targetClassName, - obj.ClassName)); - } - } - return objects.Select(o => o.ObjectId).Distinct(); - } - } -} diff --git a/Parse/Internal/Operation/ParseRemoveOperation.cs b/Parse/Internal/Operation/ParseRemoveOperation.cs deleted file mode 100644 index fdead1c6..00000000 --- a/Parse/Internal/Operation/ParseRemoveOperation.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; - -using Parse.Utilities; - -namespace Parse.Core.Internal -{ - public class ParseRemoveOperation : IParseFieldOperation - { - private ReadOnlyCollection objects; - public ParseRemoveOperation(IEnumerable objects) - { - this.objects = new ReadOnlyCollection(objects.Distinct().ToList()); - } - - public object Encode() - { - return new Dictionary { - {"__op", "Remove"}, - {"objects", PointerOrLocalIdEncoder.Instance.Encode(objects)} - }; - } - - public IParseFieldOperation MergeWithPrevious(IParseFieldOperation previous) - { - if (previous == null) - { - return this; - } - if (previous is ParseDeleteOperation) - { - return previous; - } - if (previous is ParseSetOperation) - { - var setOp = (ParseSetOperation) previous; - var oldList = Conversion.As>(setOp.Value); - return new ParseSetOperation(this.Apply(oldList, null)); - } - if (previous is ParseRemoveOperation) - { - var oldOp = (ParseRemoveOperation) previous; - return new ParseRemoveOperation(oldOp.Objects.Concat(objects)); - } - throw new InvalidOperationException("Operation is invalid after previous operation."); - } - - public object Apply(object oldValue, string key) - { - if (oldValue == null) - { - return new List(); - } - var oldList = Conversion.As>(oldValue); - return oldList.Except(objects, ParseFieldOperations.ParseObjectComparer).ToList(); - } - - public IEnumerable Objects - { - get - { - return objects; - } - } - } -} diff --git a/Parse/Internal/ParseCorePlugins.cs b/Parse/Internal/ParseCorePlugins.cs deleted file mode 100644 index 49980780..00000000 --- a/Parse/Internal/ParseCorePlugins.cs +++ /dev/null @@ -1,323 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Parse.Common.Internal; -using Parse.Core.Internal; - -#if DEBUG -[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Parse.Test")] -#endif - -namespace Parse.Core.Internal -{ - public class ParseCorePlugins : IParseCorePlugins - { - private static readonly object instanceMutex = new object(); - private static IParseCorePlugins instance; - public static IParseCorePlugins Instance - { - get - { - lock (instanceMutex) - { - return instance = instance ?? new ParseCorePlugins(); - } - } - set - { - lock (instanceMutex) - { - instance = value; - } - } - } - - private readonly object mutex = new object(); - - #region Server Controllers - - private IHttpClient httpClient; - private IParseCommandRunner commandRunner; - private IStorageController storageController; - - private IParseCloudCodeController cloudCodeController; - private IParseConfigController configController; - private IParseFileController fileController; - private IParseObjectController objectController; - private IParseQueryController queryController; - private IParseSessionController sessionController; - private IParseUserController userController; - private IObjectSubclassingController subclassingController; - - #endregion - - #region Current Instance Controller - - private IParseCurrentUserController currentUserController; - private IInstallationIdController installationIdController; - - #endregion - - public void Reset() - { - lock (mutex) - { - HttpClient = null; - CommandRunner = null; - StorageController = null; - - CloudCodeController = null; - FileController = null; - ObjectController = null; - SessionController = null; - UserController = null; - SubclassingController = null; - - CurrentUserController = null; - InstallationIdController = null; - } - } - - public IHttpClient HttpClient - { - get - { - lock (mutex) - { - return httpClient = httpClient ?? new HttpClient(); - } - } - set - { - lock (mutex) - { - httpClient = value; - } - } - } - - public IParseCommandRunner CommandRunner - { - get - { - lock (mutex) - { - return commandRunner = commandRunner ?? new ParseCommandRunner(HttpClient, InstallationIdController); - } - } - set - { - lock (mutex) - { - commandRunner = value; - } - } - } - - public IStorageController StorageController - { - get - { - lock (mutex) - { - return storageController = storageController ?? new StorageController(); - } - } - set - { - lock (mutex) - { - storageController = value; - } - } - } - - public IParseCloudCodeController CloudCodeController - { - get - { - lock (mutex) - { - return cloudCodeController = cloudCodeController ?? new ParseCloudCodeController(CommandRunner); - } - } - set - { - lock (mutex) - { - cloudCodeController = value; - } - } - } - - public IParseFileController FileController - { - get - { - lock (mutex) - { - return fileController = fileController ?? new ParseFileController(CommandRunner); - } - } - set - { - lock (mutex) - { - fileController = value; - } - } - } - - public IParseConfigController ConfigController - { - get - { - lock (mutex) - { - return configController ?? (configController = new ParseConfigController(CommandRunner, StorageController)); - } - } - set - { - lock (mutex) - { - configController = value; - } - } - } - - public IParseObjectController ObjectController - { - get - { - lock (mutex) - { - return objectController = objectController ?? new ParseObjectController(CommandRunner); - } - } - set - { - lock (mutex) - { - objectController = value; - } - } - } - - public IParseQueryController QueryController - { - get - { - lock (mutex) - { - return queryController ?? (queryController = new ParseQueryController(CommandRunner)); - } - } - set - { - lock (mutex) - { - queryController = value; - } - } - } - - public IParseSessionController SessionController - { - get - { - lock (mutex) - { - return sessionController = sessionController ?? new ParseSessionController(CommandRunner); - } - } - set - { - lock (mutex) - { - sessionController = value; - } - } - } - - public IParseUserController UserController - { - get - { - lock (mutex) - { - return (userController = userController ?? new ParseUserController(CommandRunner)); - } - } - set - { - lock (mutex) - { - userController = value; - } - } - } - - public IParseCurrentUserController CurrentUserController - { - get - { - lock (mutex) - { - return currentUserController = currentUserController ?? new ParseCurrentUserController(StorageController); - } - } - set - { - lock (mutex) - { - currentUserController = value; - } - } - } - - public IObjectSubclassingController SubclassingController - { - get - { - lock (mutex) - { - if (subclassingController == null) - { - subclassingController = new ObjectSubclassingController(); - subclassingController.AddRegisterHook(typeof(ParseUser), () => CurrentUserController.ClearFromMemory()); - } - return subclassingController; - } - } - set - { - lock (mutex) - { - subclassingController = value; - } - } - } - - public IInstallationIdController InstallationIdController - { - get - { - lock (mutex) - { - return installationIdController = installationIdController ?? new InstallationIdController(StorageController); - } - } - set - { - lock (mutex) - { - installationIdController = value; - } - } - } - } -} diff --git a/Parse/Internal/Push/Controller/ParsePushChannelsController.cs b/Parse/Internal/Push/Controller/ParsePushChannelsController.cs deleted file mode 100644 index c002d254..00000000 --- a/Parse/Internal/Push/Controller/ParsePushChannelsController.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Threading.Tasks; -using System.Collections.Generic; -using System.Threading; - -namespace Parse.Push.Internal -{ - internal class ParsePushChannelsController : IParsePushChannelsController - { - public Task SubscribeAsync(IEnumerable channels, CancellationToken cancellationToken) - { - ParseInstallation installation = ParseInstallation.CurrentInstallation; - installation.AddRangeUniqueToList("channels", channels); - return installation.SaveAsync(cancellationToken); - } - - public Task UnsubscribeAsync(IEnumerable channels, CancellationToken cancellationToken) - { - ParseInstallation installation = ParseInstallation.CurrentInstallation; - installation.RemoveAllFromList("channels", channels); - return installation.SaveAsync(cancellationToken); - } - } -} diff --git a/Parse/Internal/Push/Controller/ParsePushController.cs b/Parse/Internal/Push/Controller/ParsePushController.cs deleted file mode 100644 index 6d8da23a..00000000 --- a/Parse/Internal/Push/Controller/ParsePushController.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Threading.Tasks; -using System.Threading; -using System.Collections.Generic; -using Parse.Common.Internal; -using Parse.Core.Internal; - -namespace Parse.Push.Internal -{ - internal class ParsePushController : IParsePushController - { - private readonly IParseCommandRunner commandRunner; - private readonly IParseCurrentUserController currentUserController; - - public ParsePushController(IParseCommandRunner commandRunner, IParseCurrentUserController currentUserController) - { - this.commandRunner = commandRunner; - this.currentUserController = currentUserController; - } - - public Task SendPushNotificationAsync(IPushState state, CancellationToken cancellationToken) - { - return currentUserController.GetCurrentSessionTokenAsync(cancellationToken).OnSuccess(sessionTokenTask => - { - var command = new ParseCommand("push", - method: "POST", - sessionToken: sessionTokenTask.Result, - data: ParsePushEncoder.Instance.Encode(state)); - - return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken); - }).Unwrap(); - } - } -} diff --git a/Parse/Internal/Push/DeviceInfo/DeviceInfoController.cs b/Parse/Internal/Push/DeviceInfo/DeviceInfoController.cs deleted file mode 100644 index d402f41c..00000000 --- a/Parse/Internal/Push/DeviceInfo/DeviceInfoController.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Parse.Push.Internal -{ - - /// - /// Controls the device information. - /// - public class DeviceInfoController : IDeviceInfoController - { - /// - /// The device platform that the app is currently running on. - /// - public string DeviceType { get; } = Environment.OSVersion.ToString(); - - /// - /// The active time zone on the device that the app is currently running on. - /// - public string DeviceTimeZone => TimeZoneInfo.Local.StandardName; - - /// - /// The version number of the application. - /// - public string AppBuildVersion { get; } = System.Reflection.Assembly.GetEntryAssembly().GetName().Version.Build.ToString(); - - // TODO: Verify if this means Parse appId or just a unique identifier. - - /// - /// The identifier of the application - /// - public string AppIdentifier => AppDomain.CurrentDomain.FriendlyName; - - /// - /// The name of the current application. - /// - public string AppName { get; } = System.Reflection.Assembly.GetEntryAssembly().GetName().Name; - - public Task ExecuteParseInstallationSaveHookAsync(ParseInstallation installation) => Task.FromResult(null); - - public void Initialize() { } - } -} \ No newline at end of file diff --git a/Parse/Internal/Push/DeviceInfo/IDeviceInfoController.cs b/Parse/Internal/Push/DeviceInfo/IDeviceInfoController.cs deleted file mode 100644 index 2e5f4c7b..00000000 --- a/Parse/Internal/Push/DeviceInfo/IDeviceInfoController.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Parse.Push.Internal -{ - public interface IDeviceInfoController - { - string DeviceType { get; } - string DeviceTimeZone { get; } - string AppBuildVersion { get; } - string AppIdentifier { get; } - string AppName { get; } - - - /// - /// Executes platform specific hook that mutate the installation based on - /// the device platforms. - /// - /// Installation to be mutated. - /// - Task ExecuteParseInstallationSaveHookAsync(ParseInstallation installation); - - void Initialize(); - } -} diff --git a/Parse/Internal/Push/IParsePushPlugins.cs b/Parse/Internal/Push/IParsePushPlugins.cs deleted file mode 100644 index 0b0ba3e6..00000000 --- a/Parse/Internal/Push/IParsePushPlugins.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using Parse.Core.Internal; - -namespace Parse.Push.Internal -{ - public interface IParsePushPlugins - { - void Reset(); - - IParseCorePlugins CorePlugins { get; } - IParsePushChannelsController PushChannelsController { get; } - IParsePushController PushController { get; } - IParseCurrentInstallationController CurrentInstallationController { get; } - IDeviceInfoController DeviceInfoController { get; } - } -} \ No newline at end of file diff --git a/Parse/Internal/Push/Installation/Coder/ParseInstallationCoder.cs b/Parse/Internal/Push/Installation/Coder/ParseInstallationCoder.cs deleted file mode 100644 index c0024012..00000000 --- a/Parse/Internal/Push/Installation/Coder/ParseInstallationCoder.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Linq; -using System.Collections.Generic; -using Parse; -using Parse.Core.Internal; - -namespace Parse.Push.Internal -{ - public class ParseInstallationCoder : IParseInstallationCoder - { - private static readonly ParseInstallationCoder instance = new ParseInstallationCoder(); - public static ParseInstallationCoder Instance - { - get - { - return instance; - } - } - private const string ISO8601Format = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'"; - - public IDictionary Encode(ParseInstallation installation) - { - var state = installation.GetState(); - var data = PointerOrLocalIdEncoder.Instance.Encode(state.ToDictionary(x => x.Key, x => x.Value)) as IDictionary; - data["objectId"] = state.ObjectId; - if (state.CreatedAt != null) - { - data["createdAt"] = state.CreatedAt.Value.ToString(ISO8601Format); - } - if (state.UpdatedAt != null) - { - data["updatedAt"] = state.UpdatedAt.Value.ToString(ISO8601Format); - } - return data; - } - - public ParseInstallation Decode(IDictionary data) - { - var state = ParseObjectCoder.Instance.Decode(data, ParseDecoder.Instance); - return ParseObjectExtensions.FromState(state, "_Installation"); - } - } -} \ No newline at end of file diff --git a/Parse/Internal/Push/Installation/Controller/ParseCurrentInstallationController.cs b/Parse/Internal/Push/Installation/Controller/ParseCurrentInstallationController.cs deleted file mode 100644 index aab0c16e..00000000 --- a/Parse/Internal/Push/Installation/Controller/ParseCurrentInstallationController.cs +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using Parse.Common.Internal; -using Parse.Core.Internal; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Parse.Push.Internal -{ - internal class ParseCurrentInstallationController : IParseCurrentInstallationController - { - private const string ParseInstallationKey = "CurrentInstallation"; - - private readonly object mutex = new object(); - private readonly TaskQueue taskQueue = new TaskQueue(); - private readonly IInstallationIdController installationIdController; - private readonly IStorageController storageController; - private readonly IParseInstallationCoder installationCoder; - - public ParseCurrentInstallationController(IInstallationIdController installationIdController, IStorageController storageController, IParseInstallationCoder installationCoder) - { - this.installationIdController = installationIdController; - this.storageController = storageController; - this.installationCoder = installationCoder; - } - - private ParseInstallation currentInstallation; - internal ParseInstallation CurrentInstallation - { - get - { - lock (mutex) - { - return currentInstallation; - } - } - set - { - lock (mutex) - { - currentInstallation = value; - } - } - } - - public Task SetAsync(ParseInstallation installation, CancellationToken cancellationToken) - { - return taskQueue.Enqueue(toAwait => - { - return toAwait.ContinueWith(_ => - { - Task saveTask = storageController.LoadAsync().OnSuccess(storage => - { - if (installation == null) - { - return storage.Result.RemoveAsync(ParseInstallationKey); - } - else - { - var data = installationCoder.Encode(installation); - return storage.Result.AddAsync(ParseInstallationKey, Json.Encode(data)); - } - }).Unwrap(); - - CurrentInstallation = installation; - return saveTask; - }).Unwrap(); - }, cancellationToken); - } - - public Task GetAsync(CancellationToken cancellationToken) - { - ParseInstallation cachedCurrent; - cachedCurrent = CurrentInstallation; - - if (cachedCurrent != null) - { - return Task.FromResult(cachedCurrent); - } - - return taskQueue.Enqueue(toAwait => - { - return toAwait.ContinueWith(_ => - { - return storageController.LoadAsync().OnSuccess(stroage => - { - Task fetchTask; - object temp; - stroage.Result.TryGetValue(ParseInstallationKey, out temp); - var installationDataString = temp as string; - ParseInstallation installation = null; - if (installationDataString != null) - { - var installationData = Json.Parse(installationDataString) as IDictionary; - installation = installationCoder.Decode(installationData); - - fetchTask = Task.FromResult(null); - } - else - { - installation = ParseObject.Create(); - fetchTask = installationIdController.GetAsync().ContinueWith(t => - { - installation.SetIfDifferent("installationId", t.Result.ToString()); - }); - } - - CurrentInstallation = installation; - return fetchTask.ContinueWith(t => installation); - }); - }).Unwrap().Unwrap(); - }, cancellationToken); - } - - public Task ExistsAsync(CancellationToken cancellationToken) - { - if (CurrentInstallation != null) - { - return Task.FromResult(true); - } - - return taskQueue.Enqueue(toAwait => - { - return toAwait.ContinueWith(_ => - { - return storageController.LoadAsync().OnSuccess(s => s.Result.ContainsKey(ParseInstallationKey)); - }).Unwrap(); - }, cancellationToken); - } - - public bool IsCurrent(ParseInstallation installation) - { - return CurrentInstallation == installation; - } - - public void ClearFromMemory() - { - CurrentInstallation = null; - } - - public void ClearFromDisk() - { - ClearFromMemory(); - - taskQueue.Enqueue(toAwait => - { - return toAwait.ContinueWith(_ => - { - return storageController.LoadAsync().OnSuccess(storage => storage.Result.RemoveAsync(ParseInstallationKey)); - }).Unwrap().Unwrap(); - }, CancellationToken.None); - } - } -} diff --git a/Parse/Internal/Push/ParsePushModule.cs b/Parse/Internal/Push/ParsePushModule.cs deleted file mode 100644 index fd107f4c..00000000 --- a/Parse/Internal/Push/ParsePushModule.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Parse.Common.Internal; -using Parse.Core.Internal; -using System; - -namespace Parse.Push.Internal -{ - public class ParsePushModule : IParseModule - { - public void OnModuleRegistered() - { - } - - public void OnParseInitialized() - { - ParseObject.RegisterSubclass(); - - ParseCorePlugins.Instance.SubclassingController.AddRegisterHook(typeof(ParseInstallation), () => - { - ParsePushPlugins.Instance.CurrentInstallationController.ClearFromMemory(); - }); - - ParsePushPlugins.Instance.DeviceInfoController.Initialize(); - } - } -} \ No newline at end of file diff --git a/Parse/Internal/Push/ParsePushPlugins.cs b/Parse/Internal/Push/ParsePushPlugins.cs deleted file mode 100644 index aab2f0cb..00000000 --- a/Parse/Internal/Push/ParsePushPlugins.cs +++ /dev/null @@ -1,142 +0,0 @@ -using Parse.Core.Internal; - -namespace Parse.Push.Internal -{ - public class ParsePushPlugins : IParsePushPlugins - { - private static readonly object instanceMutex = new object(); - private static IParsePushPlugins instance; - public static IParsePushPlugins Instance - { - get - { - instance = instance ?? new ParsePushPlugins(); - return instance; - } - set - { - lock (instanceMutex) - { - instance = value; - } - } - } - - private readonly object mutex = new object(); - - private IParseCorePlugins corePlugins; - private IParsePushChannelsController pushChannelsController; - private IParsePushController pushController; - private IParseCurrentInstallationController currentInstallationController; - private IDeviceInfoController deviceInfoController; - - public void Reset() - { - lock (mutex) - { - CorePlugins = null; - PushChannelsController = null; - PushController = null; - CurrentInstallationController = null; - DeviceInfoController = null; - } - } - - public IParseCorePlugins CorePlugins - { - get - { - lock (mutex) - { - corePlugins = corePlugins ?? ParseCorePlugins.Instance; - return corePlugins; - } - } - set - { - lock (mutex) - { - corePlugins = value; - } - } - } - - public IParsePushChannelsController PushChannelsController - { - get - { - lock (mutex) - { - pushChannelsController = pushChannelsController ?? new ParsePushChannelsController(); - return pushChannelsController; - } - } - set - { - lock (mutex) - { - pushChannelsController = value; - } - } - } - - public IParsePushController PushController - { - get - { - lock (mutex) - { - pushController = pushController ?? new ParsePushController(CorePlugins.CommandRunner, CorePlugins.CurrentUserController); - return pushController; - } - } - set - { - lock (mutex) - { - pushController = value; - } - } - } - - public IParseCurrentInstallationController CurrentInstallationController - { - get - { - lock (mutex) - { - currentInstallationController = currentInstallationController ?? new ParseCurrentInstallationController( - CorePlugins.InstallationIdController, CorePlugins.StorageController, ParseInstallationCoder.Instance - ); - return currentInstallationController; - } - } - set - { - lock (mutex) - { - currentInstallationController = value; - } - } - } - - public IDeviceInfoController DeviceInfoController - { - get - { - lock (mutex) - { - deviceInfoController = deviceInfoController ?? new DeviceInfoController(); - return deviceInfoController; - } - } - set - { - lock (mutex) - { - deviceInfoController = value; - } - } - } - } -} diff --git a/Parse/Internal/Push/State/MutablePushState.cs b/Parse/Internal/Push/State/MutablePushState.cs deleted file mode 100644 index 9ffe47b1..00000000 --- a/Parse/Internal/Push/State/MutablePushState.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Linq; -using System.Collections.Generic; -using Parse.Common.Internal; - -namespace Parse.Push.Internal -{ - public class MutablePushState : IPushState - { - public ParseQuery Query { get; set; } - public IEnumerable Channels { get; set; } - public DateTime? Expiration { get; set; } - public TimeSpan? ExpirationInterval { get; set; } - public DateTime? PushTime { get; set; } - public IDictionary Data { get; set; } - public string Alert { get; set; } - - public IPushState MutatedClone(Action func) - { - MutablePushState clone = MutableClone(); - func(clone); - return clone; - } - - protected virtual MutablePushState MutableClone() - { - return new MutablePushState - { - Query = Query, - Channels = Channels == null ? null : new List(Channels), - Expiration = Expiration, - ExpirationInterval = ExpirationInterval, - PushTime = PushTime, - Data = Data == null ? null : new Dictionary(Data), - Alert = Alert - }; - } - - public override bool Equals(object obj) - { - if (obj == null || !(obj is MutablePushState)) - { - return false; - } - - var other = obj as MutablePushState; - return Object.Equals(this.Query, other.Query) && - this.Channels.CollectionsEqual(other.Channels) && - Object.Equals(this.Expiration, other.Expiration) && - Object.Equals(this.ExpirationInterval, other.ExpirationInterval) && - Object.Equals(this.PushTime, other.PushTime) && - this.Data.CollectionsEqual(other.Data) && - Object.Equals(this.Alert, other.Alert); - } - - public override int GetHashCode() - { - // TODO (richardross): Implement this. - return 0; - } - } -} diff --git a/Parse/Internal/Query/Controller/ParseQueryController.cs b/Parse/Internal/Query/Controller/ParseQueryController.cs deleted file mode 100644 index b8ffd10f..00000000 --- a/Parse/Internal/Query/Controller/ParseQueryController.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Linq; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Threading; -using System.Threading.Tasks; -using Parse.Common.Internal; - -namespace Parse.Core.Internal -{ - internal class ParseQueryController : IParseQueryController - { - private readonly IParseCommandRunner commandRunner; - - public ParseQueryController(IParseCommandRunner commandRunner) => this.commandRunner = commandRunner; - - public Task> FindAsync(ParseQuery query, ParseUser user, CancellationToken cancellationToken) where T : ParseObject => FindAsync(query.ClassName, query.BuildParameters(), user?.SessionToken, cancellationToken).OnSuccess(t => (from item in t.Result["results"] as IList select ParseObjectCoder.Instance.Decode(item as IDictionary, ParseDecoder.Instance))); - - public Task CountAsync(ParseQuery query, ParseUser user, CancellationToken cancellationToken) where T : ParseObject - { - var parameters = query.BuildParameters(); - parameters["limit"] = 0; - parameters["count"] = 1; - - return FindAsync(query.ClassName, parameters, user?.SessionToken, cancellationToken).OnSuccess(t => Convert.ToInt32(t.Result["count"])); - } - - public Task FirstAsync(ParseQuery query, ParseUser user, CancellationToken cancellationToken) where T : ParseObject - { - var parameters = query.BuildParameters(); - parameters["limit"] = 1; - - return FindAsync(query.ClassName, parameters, user?.SessionToken, cancellationToken).OnSuccess(t => (t.Result["results"] as IList).FirstOrDefault() as IDictionary is Dictionary item && item != null ? ParseObjectCoder.Instance.Decode(item, ParseDecoder.Instance) : null); - } - - private Task> FindAsync(string className, IDictionary parameters, string sessionToken, CancellationToken cancellationToken) => commandRunner.RunCommandAsync(new ParseCommand($"classes/{Uri.EscapeDataString(className)}?{ParseClient.BuildQueryString(parameters)}", method: "GET", sessionToken: sessionToken, data: null), cancellationToken: cancellationToken).OnSuccess(t => t.Result.Item2); - } -} diff --git a/Parse/Internal/Session/Controller/ParseSessionController.cs b/Parse/Internal/Session/Controller/ParseSessionController.cs deleted file mode 100644 index 25b48917..00000000 --- a/Parse/Internal/Session/Controller/ParseSessionController.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Parse.Common.Internal; - -namespace Parse.Core.Internal -{ - public class ParseSessionController : IParseSessionController - { - private readonly IParseCommandRunner commandRunner; - - public ParseSessionController(IParseCommandRunner commandRunner) - { - this.commandRunner = commandRunner; - } - - public Task GetSessionAsync(string sessionToken, CancellationToken cancellationToken) - { - var command = new ParseCommand("sessions/me", - method: "GET", - sessionToken: sessionToken, - data: null); - - return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => - { - return ParseObjectCoder.Instance.Decode(t.Result.Item2, ParseDecoder.Instance); - }); - } - - public Task RevokeAsync(string sessionToken, CancellationToken cancellationToken) - { - var command = new ParseCommand("logout", - method: "POST", - sessionToken: sessionToken, - data: new Dictionary()); - - return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken); - } - - public Task UpgradeToRevocableSessionAsync(string sessionToken, CancellationToken cancellationToken) - { - var command = new ParseCommand("upgradeToRevocableSession", - method: "POST", - sessionToken: sessionToken, - data: new Dictionary()); - - return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => - { - return ParseObjectCoder.Instance.Decode(t.Result.Item2, ParseDecoder.Instance); - }); - } - - public bool IsRevocableSessionToken(string sessionToken) - { - return sessionToken.Contains("r:"); - } - } -} diff --git a/Parse/Internal/Storage/Controller/IStorageController.cs b/Parse/Internal/Storage/Controller/IStorageController.cs deleted file mode 100644 index f76e4584..00000000 --- a/Parse/Internal/Storage/Controller/IStorageController.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Parse.Common.Internal -{ - /// - /// An abstraction for accessing persistent storage in the Parse SDK. - /// - public interface IStorageController - { - /// - /// Load the contents of this storage controller asynchronously. - /// - /// - Task> LoadAsync(); - - /// - /// Overwrites the contents of this storage controller asynchronously. - /// - /// - /// - Task> SaveAsync(IDictionary contents); - } - - /// - /// An interface for a dictionary that is persisted to disk asynchronously. - /// - /// They key type of the dictionary. - /// The value type of the dictionary. - public interface IStorageDictionary : IEnumerable> - { - int Count { get; } - TValue this[TKey key] { get; } - - IEnumerable Keys { get; } - IEnumerable Values { get; } - - bool ContainsKey(TKey key); - bool TryGetValue(TKey key, out TValue value); - - /// - /// Adds a key to this dictionary, and saves it asynchronously. - /// - /// The key to insert. - /// The value to insert. - /// - Task AddAsync(TKey key, TValue value); - - /// - /// Removes a key from this dictionary, and saves it asynchronously. - /// - /// - /// - Task RemoveAsync(TKey key); - } -} \ No newline at end of file diff --git a/Parse/Internal/Storage/Controller/StorageController.cs b/Parse/Internal/Storage/Controller/StorageController.cs deleted file mode 100644 index 9a159d8b..00000000 --- a/Parse/Internal/Storage/Controller/StorageController.cs +++ /dev/null @@ -1,179 +0,0 @@ -using System; -using System.Threading.Tasks; -using System.Linq; -using System.Collections.Generic; -using System.Threading; -using System.IO; -using Parse.Internal.Utilities; - -namespace Parse.Common.Internal -{ - /// - /// Implements `IStorageController` for PCL targets, based off of PCLStorage. - /// - public class StorageController : IStorageController - { - private class StorageDictionary : IStorageDictionary - { - private object mutex; - private Dictionary dictionary; - private FileInfo file; - - public StorageDictionary(FileInfo file) - { - this.file = file; - - mutex = new object(); - dictionary = new Dictionary(); - } - - internal Task SaveAsync() - { - string json; - lock (mutex) - json = Json.Encode(dictionary); - - return file.WriteToAsync(json); - } - - internal Task LoadAsync() - { - return file.ReadAllTextAsync().ContinueWith(t => - { - string text = t.Result; - Dictionary result = null; - try - { - result = Json.Parse(text) as Dictionary; - } - catch (Exception) - { - // Do nothing, JSON error. Probaby was empty string. - } - - lock (mutex) - { - dictionary = result ?? new Dictionary(); - } - }); - } - - internal void Update(IDictionary contents) - { - lock (mutex) - { - dictionary = contents.ToDictionary(p => p.Key, p => p.Value); - } - } - - public Task AddAsync(string key, object value) - { - lock (mutex) - { - dictionary[key] = value; - } - return SaveAsync(); - } - - public Task RemoveAsync(string key) - { - lock (mutex) - { - dictionary.Remove(key); - } - return SaveAsync(); - } - - public bool ContainsKey(string key) - { - lock (mutex) - { - return dictionary.ContainsKey(key); - } - } - - public IEnumerable Keys - { - get { lock (mutex) { return dictionary.Keys; } } - } - - public bool TryGetValue(string key, out object value) - { - lock (mutex) - { - return dictionary.TryGetValue(key, out value); - } - } - - public IEnumerable Values - { - get { lock (mutex) { return dictionary.Values; } } - } - - public object this[string key] - { - get { lock (mutex) { return dictionary[key]; } } - } - - public int Count - { - get { lock (mutex) { return dictionary.Count; } } - } - - public IEnumerator> GetEnumerator() - { - lock (mutex) - { - return dictionary.GetEnumerator(); - } - } - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() - { - lock (mutex) - { - return dictionary.GetEnumerator(); - } - } - } - - FileInfo File { get; } - StorageDictionary Storage { get; set; } - TaskQueue Queue { get; } = new TaskQueue { }; - - /// - /// Creates a Parse storage controller and attempts to extract a previously created settings storage file from the persistent storage location. - /// - public StorageController() => Storage = new StorageDictionary(File = StorageManager.PersistentStorageFileWrapper); - - /// - /// Creates a Parse storage controller with the provided wrapper. - /// - /// The file wrapper that the storage controller instance should target - public StorageController(FileInfo file) => File = file; - - /// - /// Loads a settings dictionary from the file wrapped by . - /// - /// A storage dictionary containing the deserialized content of the storage file targeted by the instance - public Task> LoadAsync() - { - // check if storage dictionary is already created from the controllers file (create if not) - if (Storage == null) - Storage = new StorageDictionary(File); - // load storage dictionary content async and return the resulting dictionary type - return Queue.Enqueue(toAwait => toAwait.ContinueWith(_ => Storage.LoadAsync().OnSuccess(__ => Storage as IStorageDictionary)).Unwrap(), CancellationToken.None); - } - - /// - /// - /// - /// - /// - public Task> SaveAsync(IDictionary contents) => Queue.Enqueue(toAwait => toAwait.ContinueWith(_ => - { - (Storage ?? (Storage = new StorageDictionary(File))).Update(contents); - return Storage.SaveAsync().OnSuccess(__ => Storage as IStorageDictionary); - }).Unwrap()); - } -} diff --git a/Parse/Internal/User/Controller/IParseUserController.cs b/Parse/Internal/User/Controller/IParseUserController.cs deleted file mode 100644 index ff90feb8..00000000 --- a/Parse/Internal/User/Controller/IParseUserController.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Parse.Core.Internal -{ - public interface IParseUserController - { - Task SignUpAsync(IObjectState state, - IDictionary operations, - CancellationToken cancellationToken); - - Task LogInAsync(string username, - string password, - CancellationToken cancellationToken); - - Task LogInAsync(string authType, - IDictionary data, - CancellationToken cancellationToken); - - Task GetUserAsync(string sessionToken, CancellationToken cancellationToken); - - Task RequestPasswordResetAsync(string email, CancellationToken cancellationToken); - } -} diff --git a/Parse/Internal/User/Controller/ParseCurrentUserController.cs b/Parse/Internal/User/Controller/ParseCurrentUserController.cs deleted file mode 100644 index f0c92109..00000000 --- a/Parse/Internal/User/Controller/ParseCurrentUserController.cs +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Globalization; -using System.Threading; -using System.Threading.Tasks; -using Parse.Common.Internal; - -namespace Parse.Core.Internal -{ - public class ParseCurrentUserController : IParseCurrentUserController - { - private readonly object mutex = new object(); - private readonly TaskQueue taskQueue = new TaskQueue(); - - private IStorageController storageController; - - public ParseCurrentUserController(IStorageController storageController) - { - this.storageController = storageController; - } - - private ParseUser currentUser; - public ParseUser CurrentUser - { - get - { - lock (mutex) - { - return currentUser; - } - } - set - { - lock (mutex) - { - currentUser = value; - } - } - } - - public Task SetAsync(ParseUser user, CancellationToken cancellationToken) - { - return taskQueue.Enqueue(toAwait => - { - return toAwait.ContinueWith(_ => - { - Task saveTask = null; - if (user == null) - { - saveTask = storageController - .LoadAsync() - .OnSuccess(t => t.Result.RemoveAsync("CurrentUser")) - .Unwrap(); - } - else - { - // TODO (hallucinogen): we need to use ParseCurrentCoder instead of this janky encoding - var data = user.ServerDataToJSONObjectForSerialization(); - data["objectId"] = user.ObjectId; - if (user.CreatedAt != null) - { - data["createdAt"] = user.CreatedAt.Value.ToString(ParseClient.DateFormatStrings.First(), - CultureInfo.InvariantCulture); - } - if (user.UpdatedAt != null) - { - data["updatedAt"] = user.UpdatedAt.Value.ToString(ParseClient.DateFormatStrings.First(), - CultureInfo.InvariantCulture); - } - - saveTask = storageController - .LoadAsync() - .OnSuccess(t => t.Result.AddAsync("CurrentUser", Json.Encode(data))) - .Unwrap(); - } - CurrentUser = user; - - return saveTask; - }).Unwrap(); - }, cancellationToken); - } - - public Task GetAsync(CancellationToken cancellationToken) - { - ParseUser cachedCurrent; - - lock (mutex) - { - cachedCurrent = CurrentUser; - } - - if (cachedCurrent != null) - { - return Task.FromResult(cachedCurrent); - } - - return taskQueue.Enqueue(toAwait => - { - return toAwait.ContinueWith(_ => - { - return storageController.LoadAsync().OnSuccess(t => - { - object temp; - t.Result.TryGetValue("CurrentUser", out temp); - var userDataString = temp as string; - ParseUser user = null; - if (userDataString != null) - { - var userData = Json.Parse(userDataString) as IDictionary; - var state = ParseObjectCoder.Instance.Decode(userData, ParseDecoder.Instance); - user = ParseObject.FromState(state, "_User"); - } - - CurrentUser = user; - return user; - }); - }).Unwrap(); - }, cancellationToken); - } - - public Task ExistsAsync(CancellationToken cancellationToken) - { - if (CurrentUser != null) - { - return Task.FromResult(true); - } - - return taskQueue.Enqueue(toAwait => - { - return toAwait.ContinueWith(_ => - storageController.LoadAsync().OnSuccess(t => t.Result.ContainsKey("CurrentUser")) - ).Unwrap(); - }, cancellationToken); - } - - public bool IsCurrent(ParseUser user) - { - lock (mutex) - { - return CurrentUser == user; - } - } - - public void ClearFromMemory() - { - CurrentUser = null; - } - - public void ClearFromDisk() - { - lock (mutex) - { - ClearFromMemory(); - - taskQueue.Enqueue(toAwait => - { - return toAwait.ContinueWith(_ => - { - return storageController.LoadAsync().OnSuccess(t => t.Result.RemoveAsync("CurrentUser")); - }).Unwrap().Unwrap(); - }, CancellationToken.None); - } - } - - public Task GetCurrentSessionTokenAsync(CancellationToken cancellationToken) - { - return GetAsync(cancellationToken).OnSuccess(t => - { - var user = t.Result; - return user == null ? null : user.SessionToken; - }); - } - - public Task LogOutAsync(CancellationToken cancellationToken) - { - return taskQueue.Enqueue(toAwait => - { - return toAwait.ContinueWith(_ => GetAsync(cancellationToken)).Unwrap().OnSuccess(t => - { - ClearFromDisk(); - }); - }, cancellationToken); - } - } -} diff --git a/Parse/Internal/User/Controller/ParseUserController.cs b/Parse/Internal/User/Controller/ParseUserController.cs deleted file mode 100644 index 3451b63d..00000000 --- a/Parse/Internal/User/Controller/ParseUserController.cs +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Parse.Common.Internal; - -namespace Parse.Core.Internal -{ - public class ParseUserController : IParseUserController - { - private readonly IParseCommandRunner commandRunner; - - public ParseUserController(IParseCommandRunner commandRunner) - { - this.commandRunner = commandRunner; - } - - public Task SignUpAsync(IObjectState state, - IDictionary operations, - CancellationToken cancellationToken) - { - var objectJSON = ParseObject.ToJSONObjectForSaving(operations); - - var command = new ParseCommand("classes/_User", - method: "POST", - data: objectJSON); - - return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => - { - var serverState = ParseObjectCoder.Instance.Decode(t.Result.Item2, ParseDecoder.Instance); - serverState = serverState.MutatedClone(mutableClone => - { - mutableClone.IsNew = true; - }); - return serverState; - }); - } - - public Task LogInAsync(string username, - string password, - CancellationToken cancellationToken) - { - var data = new Dictionary{ - {"username", username}, - {"password", password} - }; - - var command = new ParseCommand(string.Format("login?{0}", ParseClient.BuildQueryString(data)), - method: "GET", - data: null); - - return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => - { - var serverState = ParseObjectCoder.Instance.Decode(t.Result.Item2, ParseDecoder.Instance); - serverState = serverState.MutatedClone(mutableClone => - { - mutableClone.IsNew = t.Result.Item1 == System.Net.HttpStatusCode.Created; - }); - return serverState; - }); - } - - public Task LogInAsync(string authType, - IDictionary data, - CancellationToken cancellationToken) - { - var authData = new Dictionary(); - authData[authType] = data; - - var command = new ParseCommand("users", - method: "POST", - data: new Dictionary { - {"authData", authData} - }); - - return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => - { - var serverState = ParseObjectCoder.Instance.Decode(t.Result.Item2, ParseDecoder.Instance); - serverState = serverState.MutatedClone(mutableClone => - { - mutableClone.IsNew = t.Result.Item1 == System.Net.HttpStatusCode.Created; - }); - return serverState; - }); - } - - public Task GetUserAsync(string sessionToken, CancellationToken cancellationToken) - { - var command = new ParseCommand("users/me", - method: "GET", - sessionToken: sessionToken, - data: null); - - return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(t => - { - return ParseObjectCoder.Instance.Decode(t.Result.Item2, ParseDecoder.Instance); - }); - } - - public Task RequestPasswordResetAsync(string email, CancellationToken cancellationToken) - { - var command = new ParseCommand("requestPasswordReset", - method: "POST", - data: new Dictionary { - {"email", email} - }); - - return commandRunner.RunCommandAsync(command, cancellationToken: cancellationToken); - } - } -} diff --git a/Parse/Internal/Utilities/FlexibleDictionaryWrapper.cs b/Parse/Internal/Utilities/FlexibleDictionaryWrapper.cs deleted file mode 100644 index 0a155c76..00000000 --- a/Parse/Internal/Utilities/FlexibleDictionaryWrapper.cs +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System.Collections.Generic; -using System.Linq; -using Parse.Utilities; - -namespace Parse.Common.Internal -{ - /// - /// Provides a Dictionary implementation that can delegate to any other - /// dictionary, regardless of its value type. Used for coercion of - /// dictionaries when returning them to users. - /// - /// The resulting type of value in the dictionary. - /// The original type of value in the dictionary. - [Preserve(AllMembers = true, Conditional = false)] - public class FlexibleDictionaryWrapper : IDictionary - { - private readonly IDictionary toWrap; - public FlexibleDictionaryWrapper(IDictionary toWrap) - { - this.toWrap = toWrap; - } - - public void Add(string key, TOut value) - { - toWrap.Add(key, (TIn) Conversion.ConvertTo(value)); - } - - public bool ContainsKey(string key) - { - return toWrap.ContainsKey(key); - } - - public ICollection Keys - { - get { return toWrap.Keys; } - } - - public bool Remove(string key) - { - return toWrap.Remove(key); - } - - public bool TryGetValue(string key, out TOut value) - { - TIn outValue; - bool result = toWrap.TryGetValue(key, out outValue); - value = (TOut) Conversion.ConvertTo(outValue); - return result; - } - - public ICollection Values - { - get - { - return toWrap.Values - .Select(item => (TOut) Conversion.ConvertTo(item)).ToList(); - } - } - - public TOut this[string key] - { - get - { - return (TOut) Conversion.ConvertTo(toWrap[key]); - } - set - { - toWrap[key] = (TIn) Conversion.ConvertTo(value); - } - } - - public void Add(KeyValuePair item) - { - toWrap.Add(new KeyValuePair(item.Key, - (TIn) Conversion.ConvertTo(item.Value))); - } - - public void Clear() - { - toWrap.Clear(); - } - - public bool Contains(KeyValuePair item) - { - return toWrap.Contains(new KeyValuePair(item.Key, - (TIn) Conversion.ConvertTo(item.Value))); - } - - public void CopyTo(KeyValuePair[] array, int arrayIndex) - { - var converted = from pair in toWrap - select new KeyValuePair(pair.Key, - (TOut) Conversion.ConvertTo(pair.Value)); - converted.ToList().CopyTo(array, arrayIndex); - } - - public int Count - { - get { return toWrap.Count; } - } - - public bool IsReadOnly - { - get { return toWrap.IsReadOnly; } - } - - public bool Remove(KeyValuePair item) - { - return toWrap.Remove(new KeyValuePair(item.Key, - (TIn) Conversion.ConvertTo(item.Value))); - } - - public IEnumerator> GetEnumerator() - { - foreach (var pair in toWrap) - { - yield return new KeyValuePair(pair.Key, - (TOut) Conversion.ConvertTo(pair.Value)); - } - } - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() - { - return this.GetEnumerator(); - } - } -} diff --git a/Parse/Internal/Utilities/FlexibleListWrapper.cs b/Parse/Internal/Utilities/FlexibleListWrapper.cs deleted file mode 100644 index 8fc5afef..00000000 --- a/Parse/Internal/Utilities/FlexibleListWrapper.cs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using Parse.Utilities; - -namespace Parse.Common.Internal -{ - /// - /// Provides a List implementation that can delegate to any other - /// list, regardless of its value type. Used for coercion of - /// lists when returning them to users. - /// - /// The resulting type of value in the list. - /// The original type of value in the list. - [Preserve(AllMembers = true, Conditional = false)] - public class FlexibleListWrapper : IList - { - private IList toWrap; - public FlexibleListWrapper(IList toWrap) - { - this.toWrap = toWrap; - } - - public int IndexOf(TOut item) - { - return toWrap.IndexOf((TIn) Conversion.ConvertTo(item)); - } - - public void Insert(int index, TOut item) - { - toWrap.Insert(index, (TIn) Conversion.ConvertTo(item)); - } - - public void RemoveAt(int index) - { - toWrap.RemoveAt(index); - } - - public TOut this[int index] - { - get - { - return (TOut) Conversion.ConvertTo(toWrap[index]); - } - set - { - toWrap[index] = (TIn) Conversion.ConvertTo(value); - } - } - - public void Add(TOut item) - { - toWrap.Add((TIn) Conversion.ConvertTo(item)); - } - - public void Clear() - { - toWrap.Clear(); - } - - public bool Contains(TOut item) - { - return toWrap.Contains((TIn) Conversion.ConvertTo(item)); - } - - public void CopyTo(TOut[] array, int arrayIndex) - { - toWrap.Select(item => (TOut) Conversion.ConvertTo(item)) - .ToList().CopyTo(array, arrayIndex); - } - - public int Count - { - get { return toWrap.Count; } - } - - public bool IsReadOnly - { - get { return toWrap.IsReadOnly; } - } - - public bool Remove(TOut item) - { - return toWrap.Remove((TIn) Conversion.ConvertTo(item)); - } - - public IEnumerator GetEnumerator() - { - foreach (var item in (IEnumerable) toWrap) - { - yield return (TOut) Conversion.ConvertTo(item); - } - } - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() - { - return this.GetEnumerator(); - } - } -} diff --git a/Parse/Internal/Utilities/InternalExtensions.cs b/Parse/Internal/Utilities/InternalExtensions.cs deleted file mode 100644 index 02466211..00000000 --- a/Parse/Internal/Utilities/InternalExtensions.cs +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Runtime.ExceptionServices; -using System.Text; -using System.Threading.Tasks; - -namespace Parse.Common.Internal -{ - /// - /// Provides helper methods that allow us to use terser code elsewhere. - /// - public static class InternalExtensions - { - /// - /// Ensures a task (even null) is awaitable. - /// - /// - /// - /// - public static Task Safe(this Task task) - { - return task ?? Task.FromResult(default(T)); - } - - /// - /// Ensures a task (even null) is awaitable. - /// - /// - /// - public static Task Safe(this Task task) - { - return task ?? Task.FromResult(null); - } - - public delegate void PartialAccessor(ref T arg); - - public static TValue GetOrDefault(this IDictionary self, - TKey key, - TValue defaultValue) - { - TValue value; - if (self.TryGetValue(key, out value)) - { - return value; - } - return defaultValue; - } - - public static bool CollectionsEqual(this IEnumerable a, IEnumerable b) - { - return Object.Equals(a, b) || - (a != null && b != null && - a.SequenceEqual(b)); - } - - public static Task OnSuccess(this Task task, - Func, TResult> continuation) - { - return ((Task) task).OnSuccess(t => continuation((Task) t)); - } - - public static Task OnSuccess(this Task task, Action> continuation) - { - return task.OnSuccess((Func, object>) (t => - { - continuation(t); - return null; - })); - } - - public static Task OnSuccess(this Task task, - Func continuation) - { - return task.ContinueWith(t => - { - if (t.IsFaulted) - { - var ex = t.Exception.Flatten(); - if (ex.InnerExceptions.Count == 1) - { - ExceptionDispatchInfo.Capture(ex.InnerExceptions[0]).Throw(); - } - else - { - ExceptionDispatchInfo.Capture(ex).Throw(); - } - // Unreachable - return Task.FromResult(default(TResult)); - } - else if (t.IsCanceled) - { - var tcs = new TaskCompletionSource(); - tcs.SetCanceled(); - return tcs.Task; - } - else - { - return Task.FromResult(continuation(t)); - } - }).Unwrap(); - } - - public static Task OnSuccess(this Task task, Action continuation) - { - return task.OnSuccess((Func) (t => - { - continuation(t); - return null; - })); - } - - public static Task WhileAsync(Func> predicate, Func body) - { - Func iterate = null; - iterate = () => - { - return predicate().OnSuccess(t => - { - if (!t.Result) - { - return Task.FromResult(0); - } - return body().OnSuccess(_ => iterate()).Unwrap(); - }).Unwrap(); - }; - return iterate(); - } - } -} diff --git a/Parse/Internal/Utilities/ParseConfigExtensions.cs b/Parse/Internal/Utilities/ParseConfigExtensions.cs deleted file mode 100644 index 8e1a3072..00000000 --- a/Parse/Internal/Utilities/ParseConfigExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Collections.Generic; - -namespace Parse.Core.Internal -{ - /// - /// So here's the deal. We have a lot of internal APIs for ParseObject, ParseUser, etc. - /// - /// These cannot be 'internal' anymore if we are fully modularizing things out, because - /// they are no longer a part of the same library, especially as we create things like - /// Installation inside push library. - /// - /// So this class contains a bunch of extension methods that can live inside another - /// namespace, which 'wrap' the intenral APIs that already exist. - /// - public static class ParseConfigExtensions - { - public static ParseConfig Create(IDictionary fetchedConfig) - { - return new ParseConfig(fetchedConfig); - } - } -} diff --git a/Parse/Internal/Utilities/ParseObjectExtensions.cs b/Parse/Internal/Utilities/ParseObjectExtensions.cs deleted file mode 100644 index 9436a542..00000000 --- a/Parse/Internal/Utilities/ParseObjectExtensions.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Collections.Generic; - -namespace Parse.Core.Internal -{ - /// - /// So here's the deal. We have a lot of internal APIs for ParseObject, ParseUser, etc. - /// - /// These cannot be 'internal' anymore if we are fully modularizing things out, because - /// they are no longer a part of the same library, especially as we create things like - /// Installation inside push library. - /// - /// So this class contains a bunch of extension methods that can live inside another - /// namespace, which 'wrap' the intenral APIs that already exist. - /// - public static class ParseObjectExtensions - { - public static T FromState(IObjectState state, string defaultClassName) where T : ParseObject - { - return ParseObject.FromState(state, defaultClassName); - } - - public static IObjectState GetState(this ParseObject obj) - { - return obj.State; - } - - public static void HandleFetchResult(this ParseObject obj, IObjectState serverState) - { - obj.HandleFetchResult(serverState); - } - - public static IDictionary GetCurrentOperations(this ParseObject obj) - { - return obj.CurrentOperations; - } - - public static IEnumerable DeepTraversal(object root, bool traverseParseObjects = false, bool yieldRoot = false) - { - return ParseObject.DeepTraversal(root, traverseParseObjects, yieldRoot); - } - - public static void SetIfDifferent(this ParseObject obj, string key, T value) - { - obj.SetIfDifferent(key, value); - } - - public static IDictionary ServerDataToJSONObjectForSerialization(this ParseObject obj) - { - return obj.ServerDataToJSONObjectForSerialization(); - } - - public static void Set(this ParseObject obj, string key, object value) - { - obj.Set(key, value); - } - } -} diff --git a/Parse/Internal/Utilities/ParseQueryExtensions.cs b/Parse/Internal/Utilities/ParseQueryExtensions.cs deleted file mode 100644 index 112c1dd8..00000000 --- a/Parse/Internal/Utilities/ParseQueryExtensions.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Collections.Generic; - -namespace Parse.Core.Internal -{ - /// - /// So here's the deal. We have a lot of internal APIs for ParseObject, ParseUser, etc. - /// - /// These cannot be 'internal' anymore if we are fully modularizing things out, because - /// they are no longer a part of the same library, especially as we create things like - /// Installation inside push library. - /// - /// So this class contains a bunch of extension methods that can live inside another - /// namespace, which 'wrap' the intenral APIs that already exist. - /// - public static class ParseQueryExtensions - { - public static string GetClassName(this ParseQuery query) where T : ParseObject - { - return query.ClassName; - } - - public static IDictionary BuildParameters(this ParseQuery query) where T : ParseObject - { - return query.BuildParameters(false); - } - - public static object GetConstraint(this ParseQuery query, string key) where T : ParseObject - { - return query.GetConstraint(key); - } - } -} diff --git a/Parse/Internal/Utilities/ParseUserExtensions.cs b/Parse/Internal/Utilities/ParseUserExtensions.cs deleted file mode 100644 index e8fc4ede..00000000 --- a/Parse/Internal/Utilities/ParseUserExtensions.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Parse.Core.Internal -{ - /// - /// So here's the deal. We have a lot of internal APIs for ParseObject, ParseUser, etc. - /// - /// These cannot be 'internal' anymore if we are fully modularizing things out, because - /// they are no longer a part of the same library, especially as we create things like - /// Installation inside push library. - /// - /// So this class contains a bunch of extension methods that can live inside another - /// namespace, which 'wrap' the intenral APIs that already exist. - /// - public static class ParseUserExtensions - { - public static IDictionary> GetAuthData(this ParseUser user) - { - return user.AuthData; - } - - public static Task UnlinkFromAsync(this ParseUser user, string authType, CancellationToken cancellationToken) - { - return user.UnlinkFromAsync(authType, cancellationToken); - } - - public static Task LogInWithAsync(string authType, CancellationToken cancellationToken) - { - return ParseUser.LogInWithAsync(authType, cancellationToken); - } - - public static Task LogInWithAsync(string authType, IDictionary data, CancellationToken cancellationToken) - { - return ParseUser.LogInWithAsync(authType, data, cancellationToken); - } - - public static Task LinkWithAsync(this ParseUser user, string authType, CancellationToken cancellationToken) - { - return user.LinkWithAsync(authType, cancellationToken); - } - - public static Task LinkWithAsync(this ParseUser user, string authType, IDictionary data, CancellationToken cancellationToken) - { - return user.LinkWithAsync(authType, data, cancellationToken); - } - - public static Task UpgradeToRevocableSessionAsync(this ParseUser user, CancellationToken cancellationToken) - { - return user.UpgradeToRevocableSessionAsync(cancellationToken); - } - } -} diff --git a/Parse/Internal/Utilities/ReflectionHelpers.cs b/Parse/Internal/Utilities/ReflectionHelpers.cs deleted file mode 100644 index 6008ce25..00000000 --- a/Parse/Internal/Utilities/ReflectionHelpers.cs +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Reflection; -using System.Collections.Generic; -using System.Linq; - -namespace Parse.Common.Internal -{ - public static class ReflectionHelpers - { - public static IEnumerable GetProperties(Type type) - { -#if MONO || UNITY - return type.GetProperties(); -#else - return type.GetRuntimeProperties(); -#endif - } - - public static MethodInfo GetMethod(Type type, string name, Type[] parameters) - { -#if MONO || UNITY - return type.GetMethod(name, parameters); -#else - return type.GetRuntimeMethod(name, parameters); -#endif - } - - public static bool IsPrimitive(Type type) - { -#if MONO || UNITY - return type.IsPrimitive; -#else - return type.GetTypeInfo().IsPrimitive; -#endif - } - - public static IEnumerable GetInterfaces(Type type) - { -#if MONO || UNITY - return type.GetInterfaces(); -#else - return type.GetTypeInfo().ImplementedInterfaces; -#endif - } - - public static bool IsConstructedGenericType(Type type) - { -#if UNITY - return type.IsGenericType && !type.IsGenericTypeDefinition; -#else - return type.IsConstructedGenericType; -#endif - } - - public static IEnumerable GetConstructors(Type type) - { -#if UNITY - BindingFlags searchFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; - return type.GetConstructors(searchFlags); -#else - return type.GetTypeInfo().DeclaredConstructors - .Where(c => (c.Attributes & MethodAttributes.Static) == 0); -#endif - } - - public static Type[] GetGenericTypeArguments(Type type) - { -#if UNITY - return type.GetGenericArguments(); -#else - return type.GenericTypeArguments; -#endif - } - - public static PropertyInfo GetProperty(Type type, string name) - { -#if MONO || UNITY - return type.GetProperty(name); -#else - return type.GetRuntimeProperty(name); -#endif - } - - /// - /// This method helps simplify the process of getting a constructor for a type. - /// A method like this exists in .NET but is not allowed in a Portable Class Library, - /// so we've built our own. - /// - /// - /// - /// - public static ConstructorInfo FindConstructor(this Type self, params Type[] parameterTypes) - { - var constructors = - from constructor in GetConstructors(self) - let parameters = constructor.GetParameters() - let types = from p in parameters select p.ParameterType - where types.SequenceEqual(parameterTypes) - select constructor; - return constructors.SingleOrDefault(); - } - - public static bool IsNullable(Type t) - { - bool isGeneric; -#if UNITY - isGeneric = t.IsGenericType && !t.IsGenericTypeDefinition; -#else - isGeneric = t.IsConstructedGenericType; -#endif - return isGeneric && t.GetGenericTypeDefinition().Equals(typeof(Nullable<>)); - } - - public static IEnumerable GetCustomAttributes(this Assembly assembly) where T : Attribute - { -#if UNITY - return assembly.GetCustomAttributes(typeof(T), false).Select(attr => attr as T); -#else - return CustomAttributeExtensions.GetCustomAttributes(assembly); -#endif - } - } -} diff --git a/Parse/Internal/Utilities/StorageManager.cs b/Parse/Internal/Utilities/StorageManager.cs deleted file mode 100644 index a921c4fd..00000000 --- a/Parse/Internal/Utilities/StorageManager.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.IO; -using System.Text; -using System.Threading.Tasks; - -namespace Parse.Internal.Utilities -{ - /// - /// A collection of utility methods and properties for writing to the app-specific persistent storage folder. - /// - internal static class StorageManager - { - static StorageManager() => AppDomain.CurrentDomain.ProcessExit += (_, __) => { if (new FileInfo(FallbackPersistentStorageFilePath) is FileInfo file && file.Exists) file.Delete(); }; - - /// - /// The path to a persistent user-specific storage location specific to the final client assembly of the Parse library. - /// - public static string PersistentStorageFilePath => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ParseClient.CurrentConfiguration.StorageConfiguration?.RelativeStorageFilePath ?? FallbackPersistentStorageFilePath)); - - /// - /// Gets the calculated persistent storage file fallback path for this app execution. - /// - public static string FallbackPersistentStorageFilePath { get; } = ParseClient.Configuration.IdentifierBasedStorageConfiguration.Fallback.RelativeStorageFilePath; - - /// - /// Asynchronously writes the provided little-endian 16-bit character string to the file wrapped by the provided instance. - /// - /// The instance wrapping the target file that is to be written to - /// The little-endian 16-bit Unicode character string (UTF-16) that is to be written to the - /// A task that completes once the write operation to the completes - public static async Task WriteToAsync(this FileInfo file, string content) - { - using (FileStream stream = new FileStream(Path.GetFullPath(file.FullName), FileMode.Create, FileAccess.Write, FileShare.Read, 4096, FileOptions.SequentialScan | FileOptions.Asynchronous)) - { - byte[] data = Encoding.Unicode.GetBytes(content); - await stream.WriteAsync(data, 0, data.Length); - } - } - - /// - /// Asynchronously read all of the little-endian 16-bit character units (UTF-16) contained within the file wrapped by the provided instance. - /// - /// The instance wrapping the target file that string content is to be read from - /// A task that should contain the little-endian 16-bit character string (UTF-16) extracted from the if the read completes successfully - public static async Task ReadAllTextAsync(this FileInfo file) - { - using (StreamReader reader = new StreamReader(file.OpenRead(), Encoding.Unicode)) - return await reader.ReadToEndAsync(); - } - - /// - /// Gets or creates the file pointed to by and returns it's wrapper as a instance. - /// - public static FileInfo PersistentStorageFileWrapper - { - get - { - Directory.CreateDirectory(PersistentStorageFilePath.Substring(0, PersistentStorageFilePath.LastIndexOf(Path.DirectorySeparatorChar))); - - FileInfo file = new FileInfo(PersistentStorageFilePath); - if (!file.Exists) - using (file.Create()) - ; // Hopefully the JIT doesn't no-op this. The behaviour of the "using" clause should dictate how the stream is closed, to make sure it happens properly. - - return file; - } - } - - /// - /// Gets the file wrapper for the specified . - /// - /// The relative path to the target file - /// An instance of wrapping the the value - public static FileInfo GetWrapperForRelativePersistentStorageFilePath(string path) - { - path = Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), path)); - - Directory.CreateDirectory(path.Substring(0, path.LastIndexOf(Path.DirectorySeparatorChar))); - return new FileInfo(path); - } - - public static async Task TransferAsync(string originFilePath, string targetFilePath) - { - if (!String.IsNullOrWhiteSpace(originFilePath) && !String.IsNullOrWhiteSpace(targetFilePath) && new FileInfo(originFilePath) is FileInfo originFile && originFile.Exists && new FileInfo(targetFilePath) is FileInfo targetFile) - using (StreamWriter writer = new StreamWriter(targetFile.OpenWrite(), Encoding.Unicode)) - using (StreamReader reader = new StreamReader(originFile.OpenRead(), Encoding.Unicode)) - await writer.WriteAsync(await reader.ReadToEndAsync()); - } - } -} diff --git a/Parse/Internal/Utilities/SynchronizedEventHandler.cs b/Parse/Internal/Utilities/SynchronizedEventHandler.cs deleted file mode 100644 index 30f8f081..00000000 --- a/Parse/Internal/Utilities/SynchronizedEventHandler.cs +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Parse.Common.Internal -{ - /// - /// Represents an event handler that calls back from the synchronization context - /// that subscribed. - /// Should look like an EventArgs, but may not inherit EventArgs if T is implemented by the Windows team. - /// - public class SynchronizedEventHandler - { - private LinkedList> delegates = - new LinkedList>(); - public void Add(Delegate del) - { - lock (delegates) - { - TaskFactory factory; - if (SynchronizationContext.Current != null) - { - factory = - new TaskFactory(CancellationToken.None, - TaskCreationOptions.None, - TaskContinuationOptions.ExecuteSynchronously, - TaskScheduler.FromCurrentSynchronizationContext()); - } - else - { - factory = Task.Factory; - } - foreach (var d in del.GetInvocationList()) - { - delegates.AddLast(new Tuple(d, factory)); - } - } - } - - public void Remove(Delegate del) - { - lock (delegates) - { - if (delegates.Count == 0) - { - return; - } - foreach (var d in del.GetInvocationList()) - { - var node = delegates.First; - while (node != null) - { - if (node.Value.Item1 == d) - { - delegates.Remove(node); - break; - } - node = node.Next; - } - } - } - } - - public Task Invoke(object sender, T args) - { - IEnumerable> toInvoke; - var toContinue = new[] { Task.FromResult(0) }; - lock (delegates) - { - toInvoke = delegates.ToList(); - } - var invocations = toInvoke - .Select(p => p.Item2.ContinueWhenAll(toContinue, - _ => p.Item1.DynamicInvoke(sender, args))) - .ToList(); - return Task.WhenAll(invocations); - } - } -} diff --git a/Parse/Internal/Utilities/XamarinAttributes.cs b/Parse/Internal/Utilities/XamarinAttributes.cs deleted file mode 100644 index d7930b06..00000000 --- a/Parse/Internal/Utilities/XamarinAttributes.cs +++ /dev/null @@ -1,409 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Parse.Common.Internal -{ - /// - /// A reimplementation of Xamarin's PreserveAttribute. - /// This allows us to support AOT and linking for Xamarin platforms. - /// - [AttributeUsage(AttributeTargets.All)] - internal class PreserveAttribute : Attribute - { - public bool AllMembers; - public bool Conditional; - } - - [AttributeUsage(AttributeTargets.All)] - internal class LinkerSafeAttribute : Attribute - { - public LinkerSafeAttribute() { } - } - - [Preserve(AllMembers = true)] - internal class PreserveWrapperTypes - { - /// - /// Exists to ensure that generic types are AOT-compiled for the conversions we support. - /// Any new value types that we add support for will need to be registered here. - /// The method itself is never called, but by virtue of the Preserve attribute being set - /// on the class, these types will be AOT-compiled. - /// - /// This also applies to Unity. - /// - private static List CreateWrapperTypes() - { - return new List { - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - - typeof(FlexibleListWrapper), - typeof(FlexibleListWrapper), - - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - - typeof(FlexibleDictionaryWrapper), - typeof(FlexibleDictionaryWrapper), - }; - } - } -} diff --git a/Parse/Parse.csproj b/Parse/Parse.csproj index 08e95b3c..e2d9d5b9 100644 --- a/Parse/Parse.csproj +++ b/Parse/Parse.csproj @@ -3,8 +3,7 @@ netstandard2.0 bin\Release\netstandard2.0\Parse.xml - 2.0.0-develop - 2.0.0 + 2.0.0-develop-0001 latest Parse @@ -13,7 +12,7 @@ https://raw.githubusercontent.com/parse-community/parse-community.github.io/master/img/favicon/favicon-194x194.png GitHub This is the official package for the Parse .NET Standard SDK. Add a cloud backend to any platform supporting .NET Standard 2.0 with this simple-to-use SDK. - Copyright © Parse 2018. All rights reserved. + Copyright © Parse 2020. All rights reserved. Parse;netstandard2.0;parse-platform;backend;sdk;netstandard;app false @@ -24,4 +23,19 @@ + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + diff --git a/Parse/Internal/Analytics/Controller/ParseAnalyticsController.cs b/Parse/Platform/Analytics/ParseAnalyticsController.cs similarity index 73% rename from Parse/Internal/Analytics/Controller/ParseAnalyticsController.cs rename to Parse/Platform/Analytics/ParseAnalyticsController.cs index 9840f1ba..1a6a36c5 100644 --- a/Parse/Internal/Analytics/Controller/ParseAnalyticsController.cs +++ b/Parse/Platform/Analytics/ParseAnalyticsController.cs @@ -4,16 +4,20 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Parse.Core.Internal; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Execution; +using Parse.Abstractions.Platform.Analytics; +using Parse.Infrastructure.Data; +using Parse.Infrastructure.Execution; -namespace Parse.Analytics.Internal +namespace Parse.Platform.Analytics { /// /// The controller for the Parse Analytics API. /// public class ParseAnalyticsController : IParseAnalyticsController { - private IParseCommandRunner Runner { get; } + IParseCommandRunner Runner { get; } /// /// Creates an instance of the Parse Analytics API controller. @@ -29,20 +33,20 @@ public class ParseAnalyticsController : IParseAnalyticsController /// The session token for the event. /// The asynchonous cancellation token. /// A that will complete successfully once the event has been set to be tracked. - public Task TrackEventAsync(string name, IDictionary dimensions, string sessionToken, CancellationToken cancellationToken) + public Task TrackEventAsync(string name, IDictionary dimensions, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default) { IDictionary data = new Dictionary { ["at"] = DateTime.Now, - ["name"] = name, + [nameof(name)] = name, }; if (dimensions != null) { - data["dimensions"] = dimensions; + data[nameof(dimensions)] = dimensions; } - return Runner.RunCommandAsync(new ParseCommand("events/" + name, "POST", sessionToken, data: PointerOrLocalIdEncoder.Instance.Encode(data) as IDictionary), cancellationToken: cancellationToken); + return Runner.RunCommandAsync(new ParseCommand($"events/{name}", "POST", sessionToken, data: PointerOrLocalIdEncoder.Instance.Encode(data, serviceHub) as IDictionary), cancellationToken: cancellationToken); } /// @@ -52,16 +56,14 @@ public Task TrackEventAsync(string name, IDictionary dimensions, /// The token of the current session. /// The asynchronous cancellation token. /// A the will complete successfully once app openings for the target push notification have been set to be tracked. - public Task TrackAppOpenedAsync(string pushHash, string sessionToken, CancellationToken cancellationToken) + public Task TrackAppOpenedAsync(string pushHash, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default) { IDictionary data = new Dictionary { ["at"] = DateTime.Now }; if (pushHash != null) - { data["push_hash"] = pushHash; - } - return Runner.RunCommandAsync(new ParseCommand("events/AppOpened", "POST", sessionToken, data: PointerOrLocalIdEncoder.Instance.Encode(data) as IDictionary), cancellationToken: cancellationToken); + return Runner.RunCommandAsync(new ParseCommand("events/AppOpened", "POST", sessionToken, data: PointerOrLocalIdEncoder.Instance.Encode(data, serviceHub) as IDictionary), cancellationToken: cancellationToken); } } } diff --git a/Parse/Platform/Cloud/ParseCloudCodeController.cs b/Parse/Platform/Cloud/ParseCloudCodeController.cs new file mode 100644 index 00000000..2eeef0cf --- /dev/null +++ b/Parse/Platform/Cloud/ParseCloudCodeController.cs @@ -0,0 +1,31 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure.Data; +using Parse.Abstractions.Infrastructure.Execution; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Platform.Cloud; +using Parse.Infrastructure.Utilities; +using Parse.Infrastructure.Data; +using Parse.Infrastructure.Execution; + +namespace Parse.Platform.Cloud +{ + public class ParseCloudCodeController : IParseCloudCodeController + { + IParseCommandRunner CommandRunner { get; } + + IParseDataDecoder Decoder { get; } + + public ParseCloudCodeController(IParseCommandRunner commandRunner, IParseDataDecoder decoder) => (CommandRunner, Decoder) = (commandRunner, decoder); + + public Task CallFunctionAsync(string name, IDictionary parameters, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default) => CommandRunner.RunCommandAsync(new ParseCommand($"functions/{Uri.EscapeUriString(name)}", method: "POST", sessionToken: sessionToken, data: NoObjectsEncoder.Instance.Encode(parameters, serviceHub) as IDictionary), cancellationToken: cancellationToken).OnSuccess(task => + { + IDictionary decoded = Decoder.Decode(task.Result.Item2, serviceHub) as IDictionary; + return !decoded.ContainsKey("result") ? default : Conversion.To(decoded["result"]); + }); + } +} diff --git a/Parse/Platform/Configuration/ParseConfiguration.cs b/Parse/Platform/Configuration/ParseConfiguration.cs new file mode 100644 index 00000000..67ccd10b --- /dev/null +++ b/Parse/Platform/Configuration/ParseConfiguration.cs @@ -0,0 +1,80 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System.Collections.Generic; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Data; +using Parse.Infrastructure.Data; +using Parse.Infrastructure.Utilities; + +namespace Parse.Platform.Configuration +{ + /// + /// The ParseConfig is a representation of the remote configuration object, + /// that enables you to add things like feature gating, a/b testing or simple "Message of the day". + /// + public class ParseConfiguration : IJsonConvertible + { + IDictionary Properties { get; } = new Dictionary { }; + + IServiceHub Services { get; } + + internal ParseConfiguration(IServiceHub serviceHub) => Services = serviceHub; + + ParseConfiguration(IDictionary properties, IServiceHub serviceHub) : this(serviceHub) => Properties = properties; + + internal static ParseConfiguration Create(IDictionary configurationData, IParseDataDecoder decoder, IServiceHub serviceHub) => new ParseConfiguration(decoder.Decode(configurationData["params"], serviceHub) as IDictionary, serviceHub); + + /// + /// Gets a value for the key of a particular type. + /// + /// The type to convert the value to. Supported types are + /// ParseObject and its descendents, Parse types such as ParseRelation and ParseGeopoint, + /// primitive types,IList<T>, IDictionary<string, T> and strings. + /// The key of the element to get. + /// The property is retrieved + /// and is not found. + /// The property under this + /// key was found, but of a different type. + public T Get(string key) => Conversion.To(Properties[key]); + + /// + /// Populates result with the value for the key, if possible. + /// + /// The desired type for the value. + /// The key to retrieve a value for. + /// The value for the given key, converted to the + /// requested type, or null if unsuccessful. + /// true if the lookup and conversion succeeded, otherwise false. + public bool TryGetValue(string key, out T result) + { + if (Properties.ContainsKey(key)) + try + { + T temp = Conversion.To(Properties[key]); + result = temp; + return true; + } + catch + { + // Could not convert, do nothing. + } + + result = default; + return false; + } + + /// + /// Gets a value on the config. + /// + /// The key for the parameter. + /// The property is + /// retrieved and is not found. + /// The value for the key. + virtual public object this[string key] => Properties[key]; + + IDictionary IJsonConvertible.ConvertToJSON() => new Dictionary + { + ["params"] = NoObjectsEncoder.Instance.Encode(Properties, Services) + }; + } +} diff --git a/Parse/Platform/Configuration/ParseConfigurationController.cs b/Parse/Platform/Configuration/ParseConfigurationController.cs new file mode 100644 index 00000000..7cfe3d1e --- /dev/null +++ b/Parse/Platform/Configuration/ParseConfigurationController.cs @@ -0,0 +1,47 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure.Data; +using Parse.Abstractions.Infrastructure.Execution; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Platform.Configuration; +using Parse.Infrastructure.Utilities; +using Parse; +using Parse.Infrastructure.Execution; + +namespace Parse.Platform.Configuration +{ + /// + /// Config controller. + /// + internal class ParseConfigurationController : IParseConfigurationController + { + IParseCommandRunner CommandRunner { get; } + + IParseDataDecoder Decoder { get; } + + public IParseCurrentConfigurationController CurrentConfigurationController { get; } + + /// + /// Initializes a new instance of the class. + /// + public ParseConfigurationController(IParseCommandRunner commandRunner, ICacheController storageController, IParseDataDecoder decoder) + { + CommandRunner = commandRunner; + CurrentConfigurationController = new ParseCurrentConfigurationController(storageController, decoder); + Decoder = decoder; + } + + public Task FetchConfigAsync(string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default) => CommandRunner.RunCommandAsync(new ParseCommand("config", method: "GET", sessionToken: sessionToken, data: default), cancellationToken: cancellationToken).OnSuccess(task => + { + cancellationToken.ThrowIfCancellationRequested(); + return Decoder.BuildConfiguration(task.Result.Item2, serviceHub); + }).OnSuccess(task => + { + cancellationToken.ThrowIfCancellationRequested(); + CurrentConfigurationController.SetCurrentConfigAsync(task.Result); + return task; + }).Unwrap(); + } +} diff --git a/Parse/Platform/Configuration/ParseCurrentConfigurationController.cs b/Parse/Platform/Configuration/ParseCurrentConfigurationController.cs new file mode 100644 index 00000000..de21b64e --- /dev/null +++ b/Parse/Platform/Configuration/ParseCurrentConfigurationController.cs @@ -0,0 +1,57 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure.Data; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Platform.Configuration; +using Parse.Infrastructure.Utilities; + +namespace Parse.Platform.Configuration +{ + /// + /// Parse current config controller. + /// + internal class ParseCurrentConfigurationController : IParseCurrentConfigurationController + { + static string CurrentConfigurationKey { get; } = "CurrentConfig"; + + TaskQueue TaskQueue { get; } + + ParseConfiguration CurrentConfiguration { get; set; } + + ICacheController StorageController { get; } + + IParseDataDecoder Decoder { get; } + + /// + /// Initializes a new instance of the class. + /// + public ParseCurrentConfigurationController(ICacheController storageController, IParseDataDecoder decoder) + { + StorageController = storageController; + Decoder = decoder; + TaskQueue = new TaskQueue { }; + } + + public Task GetCurrentConfigAsync(IServiceHub serviceHub) => TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => CurrentConfiguration is { } ? Task.FromResult(CurrentConfiguration) : StorageController.LoadAsync().OnSuccess(task => + { + task.Result.TryGetValue(CurrentConfigurationKey, out object data); + return CurrentConfiguration = data is string { } configuration ? Decoder.BuildConfiguration(ParseClient.DeserializeJsonString(configuration), serviceHub) : new ParseConfiguration(serviceHub); + })), CancellationToken.None).Unwrap(); + + public Task SetCurrentConfigAsync(ParseConfiguration target) => TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => + { + CurrentConfiguration = target; + return StorageController.LoadAsync().OnSuccess(task => task.Result.AddAsync(CurrentConfigurationKey, ParseClient.SerializeJsonString(((IJsonConvertible) target).ConvertToJSON()))); + }).Unwrap().Unwrap(), CancellationToken.None); + + public Task ClearCurrentConfigAsync() => TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => + { + CurrentConfiguration = null; + return StorageController.LoadAsync().OnSuccess(task => task.Result.RemoveAsync(CurrentConfigurationKey)); + }).Unwrap().Unwrap(), CancellationToken.None); + + public Task ClearCurrentConfigInMemoryAsync() => TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => CurrentConfiguration = null), CancellationToken.None); + } +} diff --git a/Parse/Platform/Files/FileState.cs b/Parse/Platform/Files/FileState.cs new file mode 100644 index 00000000..9cebbc76 --- /dev/null +++ b/Parse/Platform/Files/FileState.cs @@ -0,0 +1,32 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; + +namespace Parse.Platform.Files +{ + public class FileState + { + static string SecureHyperTextTransferScheme { get; } = "https"; + + public string Name { get; set; } + + public string MediaType { get; set; } + + public Uri Location { get; set; } + + public Uri SecureLocation => Location switch + { +#warning Investigate if the first branch of this swhich expression should be removed or an explicit failure case when not testing. + + { Host: "files.parsetfss.com" } location => new UriBuilder(location) + { + Scheme = SecureHyperTextTransferScheme, + + // This makes URIBuilder assign the default port for the URL scheme. + + Port = -1, + }.Uri, + _ => Location + }; + } +} diff --git a/Parse/Public/ParseFile.cs b/Parse/Platform/Files/ParseFile.cs similarity index 62% rename from Parse/Public/ParseFile.cs rename to Parse/Platform/Files/ParseFile.cs index 7928ba5f..4d7602bb 100644 --- a/Parse/Public/ParseFile.cs +++ b/Parse/Platform/Files/ParseFile.cs @@ -1,16 +1,47 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. -using Parse.Core.Internal; using System; using System.Collections.Generic; using System.IO; -using System.Net; using System.Threading; using System.Threading.Tasks; -using Parse.Common.Internal; +using Parse.Abstractions.Infrastructure; +using Parse.Infrastructure.Utilities; +using Parse.Platform.Files; namespace Parse { + public static class FileServiceExtensions + { + /// + /// Saves the file to the Parse cloud. + /// + /// The cancellation token. + public static Task SaveFileAsync(this IServiceHub serviceHub, ParseFile file, CancellationToken cancellationToken = default) => serviceHub.SaveFileAsync(file, default, cancellationToken); + + /// + /// Saves the file to the Parse cloud. + /// + /// The progress callback. + /// The cancellation token. + public static Task SaveFileAsync(this IServiceHub serviceHub, ParseFile file, IProgress progress, CancellationToken cancellationToken = default) => file.TaskQueue.Enqueue(toAwait => serviceHub.FileController.SaveAsync(file.State, file.DataStream, serviceHub.GetCurrentSessionToken(), progress, cancellationToken), cancellationToken).OnSuccess(task => file.State = task.Result); + +#warning Make serviceHub null by default once dependents properly inject it when needed. + + /// + /// Saves the file to the Parse cloud. + /// + /// The cancellation token. + public static Task SaveAsync(this ParseFile file, IServiceHub serviceHub, CancellationToken cancellationToken = default) => serviceHub.SaveFileAsync(file, cancellationToken); + + /// + /// Saves the file to the Parse cloud. + /// + /// The progress callback. + /// The cancellation token. + public static Task SaveAsync(this ParseFile file, IServiceHub serviceHub, IProgress progress, CancellationToken cancellationToken = default) => serviceHub.SaveFileAsync(file, progress, cancellationToken); + } + /// /// ParseFile is a local representation of a file that is saved to the Parse cloud. /// @@ -29,21 +60,22 @@ namespace Parse /// public class ParseFile : IJsonConvertible { - private FileState state; - private readonly Stream dataStream; - private readonly TaskQueue taskQueue = new TaskQueue(); + internal FileState State { get; set; } + + internal Stream DataStream { get; } + + internal TaskQueue TaskQueue { get; } = new TaskQueue { }; #region Constructor - internal ParseFile(string name, Uri uri, string mimeType = null) +#warning Make IServiceHub optionally null once all dependents are injecting it if necessary. + + internal ParseFile(string name, Uri uri, string mimeType = null) => State = new FileState { - state = new FileState - { - Name = name, - Url = uri, - MimeType = mimeType - }; - } + Name = name, + Location = uri, + MediaType = mimeType + }; /// /// Creates a new file from a byte array and a name. @@ -54,8 +86,7 @@ internal ParseFile(string name, Uri uri, string mimeType = null) /// The file's data. /// To specify the content-type used when uploading the /// file, provide this parameter. - public ParseFile(string name, byte[] data, string mimeType = null) - : this(name, new MemoryStream(data), mimeType) { } + public ParseFile(string name, byte[] data, string mimeType = null) : this(name, new MemoryStream(data), mimeType) { } /// /// Creates a new file from a stream and a name. @@ -68,12 +99,13 @@ public ParseFile(string name, byte[] data, string mimeType = null) /// file, provide this parameter. public ParseFile(string name, Stream data, string mimeType = null) { - state = new FileState + State = new FileState { Name = name, - MimeType = mimeType + MediaType = mimeType }; - this.dataStream = data; + + DataStream = data; } #endregion @@ -83,121 +115,44 @@ public ParseFile(string name, Stream data, string mimeType = null) /// /// Gets whether the file still needs to be saved. /// - public bool IsDirty - { - get - { - return state.Url == null; - } - } + public bool IsDirty => State.Location == null; /// /// Gets the name of the file. Before save is called, this is the filename given by /// the user. After save is called, that name gets prefixed with a unique identifier. /// [ParseFieldName("name")] - public string Name - { - get - { - return state.Name; - } - } + public string Name => State.Name; /// /// Gets the MIME type of the file. This is either passed in to the constructor or /// inferred from the file extension. "unknown/unknown" will be used if neither is /// available. /// - public string MimeType - { - get - { - return state.MimeType; - } - } + public string MimeType => State.MediaType; /// /// Gets the url of the file. It is only available after you save the file or after /// you get the file from a . /// [ParseFieldName("url")] - public Uri Url - { - get - { - return state.SecureUrl; - } - } - - internal static IParseFileController FileController - { - get - { - return ParseCorePlugins.Instance.FileController; - } - } + public Uri Url => State.SecureLocation; #endregion - IDictionary IJsonConvertible.ToJSON() + IDictionary IJsonConvertible.ConvertToJSON() { - if (this.IsDirty) + if (IsDirty) { - throw new InvalidOperationException( - "ParseFile must be saved before it can be serialized."); + throw new InvalidOperationException("ParseFile must be saved before it can be serialized."); } - return new Dictionary { - {"__type", "File"}, - {"name", Name}, - {"url", Url.AbsoluteUri} - }; - } - - #region Save - - /// - /// Saves the file to the Parse cloud. - /// - public Task SaveAsync() - { - return SaveAsync(null, CancellationToken.None); - } - - /// - /// Saves the file to the Parse cloud. - /// - /// The cancellation token. - public Task SaveAsync(CancellationToken cancellationToken) - { - return SaveAsync(null, cancellationToken); - } - /// - /// Saves the file to the Parse cloud. - /// - /// The progress callback. - public Task SaveAsync(IProgress progress) - { - return SaveAsync(progress, CancellationToken.None); - } - - /// - /// Saves the file to the Parse cloud. - /// - /// The progress callback. - /// The cancellation token. - public Task SaveAsync(IProgress progress, - CancellationToken cancellationToken) - { - return taskQueue.Enqueue( - toAwait => FileController.SaveAsync(state, dataStream, ParseUser.CurrentSessionToken, progress, cancellationToken), cancellationToken) - .OnSuccess(t => + return new Dictionary { - state = t.Result; - }); + ["__type"] = "File", + ["name"] = Name, + ["url"] = Url.AbsoluteUri + }; } - - #endregion } } diff --git a/Parse/Platform/Files/ParseFileController.cs b/Parse/Platform/Files/ParseFileController.cs new file mode 100644 index 00000000..4f4ff0eb --- /dev/null +++ b/Parse/Platform/Files/ParseFileController.cs @@ -0,0 +1,58 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Execution; +using Parse.Abstractions.Platform.Files; +using Parse.Infrastructure.Execution; +using Parse.Infrastructure.Utilities; + +namespace Parse.Platform.Files +{ + public class ParseFileController : IParseFileController + { + IParseCommandRunner CommandRunner { get; } + + public ParseFileController(IParseCommandRunner commandRunner) => CommandRunner = commandRunner; + + public Task SaveAsync(FileState state, Stream dataStream, string sessionToken, IProgress progress, CancellationToken cancellationToken = default) + { + if (state.Location != null) + // !isDirty + + return Task.FromResult(state); + + if (cancellationToken.IsCancellationRequested) + return Task.FromCanceled(cancellationToken); + + long oldPosition = dataStream.Position; + + return CommandRunner.RunCommandAsync(new ParseCommand($"files/{state.Name}", method: "POST", sessionToken: sessionToken, contentType: state.MediaType, stream: dataStream), uploadProgress: progress, cancellationToken: cancellationToken).OnSuccess(uploadTask => + { + Tuple> result = uploadTask.Result; + IDictionary jsonData = result.Item2; + cancellationToken.ThrowIfCancellationRequested(); + + return new FileState + { + Name = jsonData["name"] as string, + Location = new Uri(jsonData["url"] as string, UriKind.Absolute), + MediaType = state.MediaType + }; + }).ContinueWith(task => + { + // Rewind the stream on failure or cancellation (if possible). + + if ((task.IsFaulted || task.IsCanceled) && dataStream.CanSeek) + dataStream.Seek(oldPosition, SeekOrigin.Begin); + + return task; + }).Unwrap(); + } + } +} diff --git a/Parse/Platform/Installations/ParseCurrentInstallationController.cs b/Parse/Platform/Installations/ParseCurrentInstallationController.cs new file mode 100644 index 00000000..c9b0f0fd --- /dev/null +++ b/Parse/Platform/Installations/ParseCurrentInstallationController.cs @@ -0,0 +1,107 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Platform.Installations; +using Parse.Abstractions.Platform.Objects; +using Parse.Infrastructure.Utilities; + +namespace Parse.Platform.Installations +{ + internal class ParseCurrentInstallationController : IParseCurrentInstallationController + { + static string ParseInstallationKey { get; } = nameof(CurrentInstallation); + + object Mutex { get; } = new object { }; + + TaskQueue TaskQueue { get; } = new TaskQueue { }; + + IParseInstallationController InstallationController { get; } + + ICacheController StorageController { get; } + + IParseInstallationCoder InstallationCoder { get; } + + IParseObjectClassController ClassController { get; } + + public ParseCurrentInstallationController(IParseInstallationController installationIdController, ICacheController storageController, IParseInstallationCoder installationCoder, IParseObjectClassController classController) + { + InstallationController = installationIdController; + StorageController = storageController; + InstallationCoder = installationCoder; + ClassController = classController; + } + + ParseInstallation CurrentInstallationValue { get; set; } + + internal ParseInstallation CurrentInstallation + { + get + { + lock (Mutex) + { + return CurrentInstallationValue; + } + } + set + { + lock (Mutex) + { + CurrentInstallationValue = value; + } + } + } + + public Task SetAsync(ParseInstallation installation, CancellationToken cancellationToken) => TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => + { + Task saveTask = StorageController.LoadAsync().OnSuccess(storage => installation is { } ? storage.Result.AddAsync(ParseInstallationKey, JsonUtilities.Encode(InstallationCoder.Encode(installation))) : storage.Result.RemoveAsync(ParseInstallationKey)).Unwrap(); + CurrentInstallation = installation; + + return saveTask; + }).Unwrap(), cancellationToken); + + public Task GetAsync(IServiceHub serviceHub, CancellationToken cancellationToken = default) + { + ParseInstallation cachedCurrent; + cachedCurrent = CurrentInstallation; + + return cachedCurrent is { } ? Task.FromResult(cachedCurrent) : TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => StorageController.LoadAsync().OnSuccess(stroage => + { + Task fetchTask; + stroage.Result.TryGetValue(ParseInstallationKey, out object temp); + ParseInstallation installation = default; + + if (temp is string installationDataString) + { + IDictionary installationData = JsonUtilities.Parse(installationDataString) as IDictionary; + installation = InstallationCoder.Decode(installationData, serviceHub); + + fetchTask = Task.FromResult(null); + } + else + { + installation = ClassController.CreateObject(serviceHub); + fetchTask = InstallationController.GetAsync().ContinueWith(t => installation.SetIfDifferent("installationId", t.Result.ToString())); + } + + CurrentInstallation = installation; + return fetchTask.ContinueWith(task => installation); + })).Unwrap().Unwrap(), cancellationToken); + } + + public Task ExistsAsync(CancellationToken cancellationToken) => CurrentInstallation is { } ? Task.FromResult(true) : TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => StorageController.LoadAsync().OnSuccess(storageTask => storageTask.Result.ContainsKey(ParseInstallationKey))).Unwrap(), cancellationToken); + + public bool IsCurrent(ParseInstallation installation) => CurrentInstallation == installation; + + public void ClearFromMemory() => CurrentInstallation = default; + + public void ClearFromDisk() + { + ClearFromMemory(); + + TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => StorageController.LoadAsync().OnSuccess(storage => storage.Result.RemoveAsync(ParseInstallationKey))).Unwrap().Unwrap(), CancellationToken.None); + } + } +} diff --git a/Parse/Platform/Installations/ParseInstallation.cs b/Parse/Platform/Installations/ParseInstallation.cs new file mode 100644 index 00000000..b42401b2 --- /dev/null +++ b/Parse/Platform/Installations/ParseInstallation.cs @@ -0,0 +1,347 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using Parse.Infrastructure.Utilities; + +namespace Parse +{ + /// + /// Represents this app installed on this device. Use this class to track information you want + /// to sample from (i.e. if you update a field on app launch, you can issue a query to see + /// the number of devices which were active in the last N hours). + /// + [ParseClassName("_Installation")] + public partial class ParseInstallation : ParseObject + { + static HashSet ImmutableKeys { get; } = new HashSet { "deviceType", "deviceUris", "installationId", "timeZone", "localeIdentifier", "parseVersion", "appName", "appIdentifier", "appVersion", "pushType" }; + + /// + /// Constructs a new ParseInstallation. Generally, you should not need to construct + /// ParseInstallations yourself. Instead use . + /// + public ParseInstallation() : base() { } + + /// + /// A GUID that uniquely names this app installed on this device. + /// + [ParseFieldName("installationId")] + public Guid InstallationId + { + get + { + string installationIdString = GetProperty(nameof(InstallationId)); + Guid? installationId = null; + + try + { + installationId = new Guid(installationIdString); + } + catch (Exception) + { + // Do nothing. + } + + return installationId.Value; + } + internal set + { + Guid installationId = value; + SetProperty(installationId.ToString(), nameof(InstallationId)); + } + } + + /// + /// The runtime target of this installation object. + /// + [ParseFieldName("deviceType")] + public string DeviceType + { + get => GetProperty(nameof(DeviceType)); + internal set => SetProperty(value, nameof(DeviceType)); + } + + /// + /// The user-friendly display name of this application. + /// + [ParseFieldName("appName")] + public string AppName + { + get => GetProperty(nameof(AppName)); + internal set => SetProperty(value, nameof(AppName)); + } + + /// + /// A version string consisting of Major.Minor.Build.Revision. + /// + [ParseFieldName("appVersion")] + public string AppVersion + { + get => GetProperty(nameof(AppVersion)); + internal set => SetProperty(value, nameof(AppVersion)); + } + + /// + /// The system-dependent unique identifier of this installation. This identifier should be + /// sufficient to distinctly name an app on stores which may allow multiple apps with the + /// same display name. + /// + [ParseFieldName("appIdentifier")] + public string AppIdentifier + { + get => GetProperty(nameof(AppIdentifier)); + internal set => SetProperty(value, nameof(AppIdentifier)); + } + + /// + /// The time zone in which this device resides. This string is in the tz database format + /// Parse uses for local-time pushes. Due to platform restrictions, the mapping is less + /// granular on Windows than it may be on other systems. E.g. The zones + /// America/Vancouver America/Dawson America/Whitehorse, America/Tijuana, PST8PDT, and + /// America/Los_Angeles are all reported as America/Los_Angeles. + /// + [ParseFieldName("timeZone")] + public string TimeZone + { + get => GetProperty(nameof(TimeZone)); + private set => SetProperty(value, nameof(TimeZone)); + } + + /// + /// The users locale. This field gets automatically populated by the SDK. + /// Can be null (Parse Push uses default language in this case). + /// + [ParseFieldName("localeIdentifier")] + public string LocaleIdentifier + { + get => GetProperty(nameof(LocaleIdentifier)); + private set => SetProperty(value, nameof(LocaleIdentifier)); + } + + /// + /// Gets the locale identifier in the format: [language code]-[COUNTRY CODE]. + /// + /// The locale identifier in the format: [language code]-[COUNTRY CODE]. + private string GetLocaleIdentifier() + { + string languageCode = null; + string countryCode = null; + + if (CultureInfo.CurrentCulture != null) + { + languageCode = CultureInfo.CurrentCulture.TwoLetterISOLanguageName; + } + if (RegionInfo.CurrentRegion != null) + { + countryCode = RegionInfo.CurrentRegion.TwoLetterISORegionName; + } + if (String.IsNullOrEmpty(countryCode)) + { + return languageCode; + } + else + { + return String.Format("{0}-{1}", languageCode, countryCode); + } + } + + /// + /// The version of the Parse SDK used to build this application. + /// + [ParseFieldName("parseVersion")] + public Version ParseVersion + { + get + { + string versionString = GetProperty(nameof(ParseVersion)); + Version version = null; + try + { + version = new Version(versionString); + } + catch (Exception) + { + // Do nothing. + } + + return version; + } + private set + { + Version version = value; + SetProperty(version.ToString(), nameof(ParseVersion)); + } + } + + /// + /// A sequence of arbitrary strings which are used to identify this installation for push notifications. + /// By convention, the empty string is known as the "Broadcast" channel. + /// + [ParseFieldName("channels")] + public IList Channels + { + get => GetProperty>(nameof(Channels)); + set => SetProperty(value, nameof(Channels)); + } + + protected override bool CheckKeyMutable(string key) => !ImmutableKeys.Contains(key); + + protected override Task SaveAsync(Task toAwait, CancellationToken cancellationToken) + { + Task platformHookTask = null; + + if (Services.CurrentInstallationController.IsCurrent(this)) + { + SetIfDifferent("deviceType", Services.MetadataController.EnvironmentData.Platform); + SetIfDifferent("timeZone", Services.MetadataController.EnvironmentData.TimeZone); + SetIfDifferent("localeIdentifier", GetLocaleIdentifier()); + SetIfDifferent("parseVersion", ParseClient.Version); + SetIfDifferent("appVersion", Services.MetadataController.HostManifestData.Version); + SetIfDifferent("appIdentifier", Services.MetadataController.HostManifestData.Identifier); + SetIfDifferent("appName", Services.MetadataController.HostManifestData.Name); + +#warning InstallationDataFinalizer needs to be injected here somehow or removed. + + //platformHookTask = Client.InstallationDataFinalizer.FinalizeAsync(this); + } + + return platformHookTask.Safe().OnSuccess(_ => base.SaveAsync(toAwait, cancellationToken)).Unwrap().OnSuccess(_ => Services.CurrentInstallationController.IsCurrent(this) ? Task.CompletedTask : Services.CurrentInstallationController.SetAsync(this, cancellationToken)).Unwrap(); + } + + /// + /// This mapping of Windows names to a standard everyone else uses is maintained + /// by the Unicode consortium, which makes this officially the first helpful + /// interaction between Unicode and Microsoft. + /// Unfortunately this is a little lossy in that we only store the first mapping in each zone because + /// Microsoft does not give us more granular location information. + /// Built from http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/zone_tzid.html + /// + internal static Dictionary TimeZoneNameMap { get; } = new Dictionary + { + ["Dateline Standard Time"] = "Etc/GMT+12", + ["UTC-11"] = "Etc/GMT+11", + ["Hawaiian Standard Time"] = "Pacific/Honolulu", + ["Alaskan Standard Time"] = "America/Anchorage", + ["Pacific Standard Time (Mexico)"] = "America/Santa_Isabel", + ["Pacific Standard Time"] = "America/Los_Angeles", + ["US Mountain Standard Time"] = "America/Phoenix", + ["Mountain Standard Time (Mexico)"] = "America/Chihuahua", + ["Mountain Standard Time"] = "America/Denver", + ["Central America Standard Time"] = "America/Guatemala", + ["Central Standard Time"] = "America/Chicago", + ["Central Standard Time (Mexico)"] = "America/Mexico_City", + ["Canada Central Standard Time"] = "America/Regina", + ["SA Pacific Standard Time"] = "America/Bogota", + ["Eastern Standard Time"] = "America/New_York", + ["US Eastern Standard Time"] = "America/Indianapolis", + ["Venezuela Standard Time"] = "America/Caracas", + ["Paraguay Standard Time"] = "America/Asuncion", + ["Atlantic Standard Time"] = "America/Halifax", + ["Central Brazilian Standard Time"] = "America/Cuiaba", + ["SA Western Standard Time"] = "America/La_Paz", + ["Pacific SA Standard Time"] = "America/Santiago", + ["Newfoundland Standard Time"] = "America/St_Johns", + ["E. South America Standard Time"] = "America/Sao_Paulo", + ["Argentina Standard Time"] = "America/Buenos_Aires", + ["SA Eastern Standard Time"] = "America/Cayenne", + ["Greenland Standard Time"] = "America/Godthab", + ["Montevideo Standard Time"] = "America/Montevideo", + ["Bahia Standard Time"] = "America/Bahia", + ["UTC-02"] = "Etc/GMT+2", + ["Azores Standard Time"] = "Atlantic/Azores", + ["Cape Verde Standard Time"] = "Atlantic/Cape_Verde", + ["Morocco Standard Time"] = "Africa/Casablanca", + ["UTC"] = "Etc/GMT", + ["GMT Standard Time"] = "Europe/London", + ["Greenwich Standard Time"] = "Atlantic/Reykjavik", + ["W. Europe Standard Time"] = "Europe/Berlin", + ["Central Europe Standard Time"] = "Europe/Budapest", + ["Romance Standard Time"] = "Europe/Paris", + ["Central European Standard Time"] = "Europe/Warsaw", + ["W. Central Africa Standard Time"] = "Africa/Lagos", + ["Namibia Standard Time"] = "Africa/Windhoek", + ["GTB Standard Time"] = "Europe/Bucharest", + ["Middle East Standard Time"] = "Asia/Beirut", + ["Egypt Standard Time"] = "Africa/Cairo", + ["Syria Standard Time"] = "Asia/Damascus", + ["E. Europe Standard Time"] = "Asia/Nicosia", + ["South Africa Standard Time"] = "Africa/Johannesburg", + ["FLE Standard Time"] = "Europe/Kiev", + ["Turkey Standard Time"] = "Europe/Istanbul", + ["Israel Standard Time"] = "Asia/Jerusalem", + ["Jordan Standard Time"] = "Asia/Amman", + ["Arabic Standard Time"] = "Asia/Baghdad", + ["Kaliningrad Standard Time"] = "Europe/Kaliningrad", + ["Arab Standard Time"] = "Asia/Riyadh", + ["E. Africa Standard Time"] = "Africa/Nairobi", + ["Iran Standard Time"] = "Asia/Tehran", + ["Arabian Standard Time"] = "Asia/Dubai", + ["Azerbaijan Standard Time"] = "Asia/Baku", + ["Russian Standard Time"] = "Europe/Moscow", + ["Mauritius Standard Time"] = "Indian/Mauritius", + ["Georgian Standard Time"] = "Asia/Tbilisi", + ["Caucasus Standard Time"] = "Asia/Yerevan", + ["Afghanistan Standard Time"] = "Asia/Kabul", + ["Pakistan Standard Time"] = "Asia/Karachi", + ["West Asia Standard Time"] = "Asia/Tashkent", + ["India Standard Time"] = "Asia/Calcutta", + ["Sri Lanka Standard Time"] = "Asia/Colombo", + ["Nepal Standard Time"] = "Asia/Katmandu", + ["Central Asia Standard Time"] = "Asia/Almaty", + ["Bangladesh Standard Time"] = "Asia/Dhaka", + ["Ekaterinburg Standard Time"] = "Asia/Yekaterinburg", + ["Myanmar Standard Time"] = "Asia/Rangoon", + ["SE Asia Standard Time"] = "Asia/Bangkok", + ["N. Central Asia Standard Time"] = "Asia/Novosibirsk", + ["China Standard Time"] = "Asia/Shanghai", + ["North Asia Standard Time"] = "Asia/Krasnoyarsk", + ["Singapore Standard Time"] = "Asia/Singapore", + ["W. Australia Standard Time"] = "Australia/Perth", + ["Taipei Standard Time"] = "Asia/Taipei", + ["Ulaanbaatar Standard Time"] = "Asia/Ulaanbaatar", + ["North Asia East Standard Time"] = "Asia/Irkutsk", + ["Tokyo Standard Time"] = "Asia/Tokyo", + ["Korea Standard Time"] = "Asia/Seoul", + ["Cen. Australia Standard Time"] = "Australia/Adelaide", + ["AUS Central Standard Time"] = "Australia/Darwin", + ["E. Australia Standard Time"] = "Australia/Brisbane", + ["AUS Eastern Standard Time"] = "Australia/Sydney", + ["West Pacific Standard Time"] = "Pacific/Port_Moresby", + ["Tasmania Standard Time"] = "Australia/Hobart", + ["Yakutsk Standard Time"] = "Asia/Yakutsk", + ["Central Pacific Standard Time"] = "Pacific/Guadalcanal", + ["Vladivostok Standard Time"] = "Asia/Vladivostok", + ["New Zealand Standard Time"] = "Pacific/Auckland", + ["UTC+12"] = "Etc/GMT-12", + ["Fiji Standard Time"] = "Pacific/Fiji", + ["Magadan Standard Time"] = "Asia/Magadan", + ["Tonga Standard Time"] = "Pacific/Tongatapu", + ["Samoa Standard Time"] = "Pacific/Apia" + }; + + /// + /// This is a mapping of odd TimeZone offsets to their respective IANA codes across the world. + /// This list was compiled from painstakingly pouring over the information available at + /// https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. + /// + internal static Dictionary TimeZoneOffsetMap { get; } = new Dictionary + { + [new TimeSpan(12, 45, 0)] = "Pacific/Chatham", + [new TimeSpan(10, 30, 0)] = "Australia/Lord_Howe", + [new TimeSpan(9, 30, 0)] = "Australia/Adelaide", + [new TimeSpan(8, 45, 0)] = "Australia/Eucla", + [new TimeSpan(8, 30, 0)] = "Asia/Pyongyang", // Parse in North Korea confirmed. + [new TimeSpan(6, 30, 0)] = "Asia/Rangoon", + [new TimeSpan(5, 45, 0)] = "Asia/Kathmandu", + [new TimeSpan(5, 30, 0)] = "Asia/Colombo", + [new TimeSpan(4, 30, 0)] = "Asia/Kabul", + [new TimeSpan(3, 30, 0)] = "Asia/Tehran", + [new TimeSpan(-3, 30, 0)] = "America/St_Johns", + [new TimeSpan(-4, 30, 0)] = "America/Caracas", + [new TimeSpan(-9, 30, 0)] = "Pacific/Marquesas", + }; + } +} diff --git a/Parse/Platform/Installations/ParseInstallationCoder.cs b/Parse/Platform/Installations/ParseInstallationCoder.cs new file mode 100644 index 00000000..25263db6 --- /dev/null +++ b/Parse/Platform/Installations/ParseInstallationCoder.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Linq; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Data; +using Parse.Abstractions.Platform.Installations; +using Parse.Abstractions.Platform.Objects; +using Parse.Infrastructure.Data; + +namespace Parse.Platform.Installations +{ + public class ParseInstallationCoder : IParseInstallationCoder + { + IParseDataDecoder Decoder { get; } + + IParseObjectClassController ClassController { get; } + + public ParseInstallationCoder(IParseDataDecoder decoder, IParseObjectClassController classController) => (Decoder, ClassController) = (decoder, classController); + + public IDictionary Encode(ParseInstallation installation) + { + IObjectState state = installation.State; + IDictionary data = PointerOrLocalIdEncoder.Instance.Encode(state.ToDictionary(pair => pair.Key, pair => pair.Value), installation.Services) as IDictionary; + + data["objectId"] = state.ObjectId; + + // The following operations use the date and time serialization format defined by ISO standard 8601. + + if (state.CreatedAt is { }) + data["createdAt"] = state.CreatedAt.Value.ToString(ParseClient.DateFormatStrings[0]); + + if (state.UpdatedAt is { }) + data["updatedAt"] = state.UpdatedAt.Value.ToString(ParseClient.DateFormatStrings[0]); + + return data; + } + + public ParseInstallation Decode(IDictionary data, IServiceHub serviceHub) => ClassController.GenerateObjectFromState(ParseObjectCoder.Instance.Decode(data, Decoder, serviceHub), "_Installation", serviceHub); + } +} \ No newline at end of file diff --git a/Parse/Platform/Installations/ParseInstallationController.cs b/Parse/Platform/Installations/ParseInstallationController.cs new file mode 100644 index 00000000..80e0fc14 --- /dev/null +++ b/Parse/Platform/Installations/ParseInstallationController.cs @@ -0,0 +1,62 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Platform.Installations; +using Parse.Infrastructure.Utilities; + +namespace Parse.Platform.Installations +{ + public class ParseInstallationController : IParseInstallationController + { + static string InstallationIdKey { get; } = "InstallationId"; + + object Mutex { get; } = new object { }; + + Guid? InstallationId { get; set; } + + ICacheController StorageController { get; } + + public ParseInstallationController(ICacheController storageController) => StorageController = storageController; + + public Task SetAsync(Guid? installationId) + { + lock (Mutex) + { +#warning Should refactor here if this operates correctly. + + Task saveTask = installationId is { } ? StorageController.LoadAsync().OnSuccess(storage => storage.Result.AddAsync(InstallationIdKey, installationId.ToString())).Unwrap() : StorageController.LoadAsync().OnSuccess(storage => storage.Result.RemoveAsync(InstallationIdKey)).Unwrap(); + + InstallationId = installationId; + return saveTask; + } + } + + public Task GetAsync() + { + lock (Mutex) + if (InstallationId is { }) + return Task.FromResult(InstallationId); + + return StorageController.LoadAsync().OnSuccess(storageTask => + { + storageTask.Result.TryGetValue(InstallationIdKey, out object id); + + try + { + lock (Mutex) + return Task.FromResult(InstallationId = new Guid(id as string)); + } + catch (Exception) + { + Guid newInstallationId = Guid.NewGuid(); + return SetAsync(newInstallationId).OnSuccess(_ => newInstallationId); + } + }) + .Unwrap(); + } + + public Task ClearAsync() => SetAsync(null); + } +} diff --git a/Parse/Platform/Installations/ParseInstallationDataFinalizer.cs b/Parse/Platform/Installations/ParseInstallationDataFinalizer.cs new file mode 100644 index 00000000..4f6d335f --- /dev/null +++ b/Parse/Platform/Installations/ParseInstallationDataFinalizer.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; +using Parse.Abstractions.Platform.Installations; + +namespace Parse.Platform.Installations +{ + /// + /// Controls the device information. + /// + public class ParseInstallationDataFinalizer : IParseInstallationDataFinalizer + { + public Task FinalizeAsync(ParseInstallation installation) => Task.FromResult(null); + + public void Initialize() { } + } +} \ No newline at end of file diff --git a/Parse/Public/ParseGeoDistance.cs b/Parse/Platform/Location/ParseGeoDistance.cs similarity index 73% rename from Parse/Public/ParseGeoDistance.cs rename to Parse/Platform/Location/ParseGeoDistance.cs index 75b55bfd..33f31fc4 100644 --- a/Parse/Public/ParseGeoDistance.cs +++ b/Parse/Platform/Location/ParseGeoDistance.cs @@ -15,10 +15,7 @@ public struct ParseGeoDistance /// /// The distance in radians. public ParseGeoDistance(double radians) - : this() - { - Radians = radians; - } + : this() => Radians = radians; /// /// Gets the distance in radians. @@ -28,53 +25,32 @@ public ParseGeoDistance(double radians) /// /// Gets the distance in miles. /// - public double Miles - { - get - { - return Radians * EarthMeanRadiusMiles; - } - } + public double Miles => Radians * EarthMeanRadiusMiles; /// /// Gets the distance in kilometers. /// - public double Kilometers - { - get - { - return Radians * EarthMeanRadiusKilometers; - } - } + public double Kilometers => Radians * EarthMeanRadiusKilometers; /// /// Gets a ParseGeoDistance from a number of miles. /// /// The number of miles. /// A ParseGeoDistance for the given number of miles. - public static ParseGeoDistance FromMiles(double miles) - { - return new ParseGeoDistance(miles / EarthMeanRadiusMiles); - } + public static ParseGeoDistance FromMiles(double miles) => new ParseGeoDistance(miles / EarthMeanRadiusMiles); /// /// Gets a ParseGeoDistance from a number of kilometers. /// /// The number of kilometers. /// A ParseGeoDistance for the given number of kilometers. - public static ParseGeoDistance FromKilometers(double kilometers) - { - return new ParseGeoDistance(kilometers / EarthMeanRadiusKilometers); - } + public static ParseGeoDistance FromKilometers(double kilometers) => new ParseGeoDistance(kilometers / EarthMeanRadiusKilometers); /// /// Gets a ParseGeoDistance from a number of radians. /// /// The number of radians. /// A ParseGeoDistance for the given number of radians. - public static ParseGeoDistance FromRadians(double radians) - { - return new ParseGeoDistance(radians); - } + public static ParseGeoDistance FromRadians(double radians) => new ParseGeoDistance(radians); } } diff --git a/Parse/Public/ParseGeoPoint.cs b/Parse/Platform/Location/ParseGeoPoint.cs similarity index 89% rename from Parse/Public/ParseGeoPoint.cs rename to Parse/Platform/Location/ParseGeoPoint.cs index 65f59555..eead203a 100644 --- a/Parse/Public/ParseGeoPoint.cs +++ b/Parse/Platform/Location/ParseGeoPoint.cs @@ -2,8 +2,7 @@ using System; using System.Collections.Generic; -using Parse.Core.Internal; -using Parse.Common.Internal; +using Parse.Abstractions.Infrastructure; namespace Parse { @@ -35,10 +34,7 @@ public ParseGeoPoint(double latitude, double longitude) /// public double Latitude { - get - { - return latitude; - } + get => latitude; set { if (value > 90 || value < -90) @@ -57,10 +53,7 @@ public double Latitude /// public double Longitude { - get - { - return longitude; - } + get => longitude; set { if (value > 180 || value < -180) @@ -97,13 +90,10 @@ public ParseGeoDistance DistanceTo(ParseGeoPoint point) return new ParseGeoDistance(2 * Math.Asin(Math.Sqrt(a))); } - IDictionary IJsonConvertible.ToJSON() - { - return new Dictionary { + IDictionary IJsonConvertible.ConvertToJSON() => new Dictionary { {"__type", "GeoPoint"}, - {"latitude", Latitude}, - {"longitude", Longitude} + {nameof(latitude), Latitude}, + {nameof(longitude), Longitude} }; - } } } diff --git a/Parse/Platform/Objects/MutableObjectState.cs b/Parse/Platform/Objects/MutableObjectState.cs new file mode 100644 index 00000000..f8ad5bd9 --- /dev/null +++ b/Parse/Platform/Objects/MutableObjectState.cs @@ -0,0 +1,75 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections.Generic; +using System.Linq; +using Parse.Abstractions.Infrastructure.Control; +using Parse.Abstractions.Platform.Objects; +using Parse.Infrastructure.Control; + +namespace Parse.Platform.Objects +{ + public class MutableObjectState : IObjectState + { + public bool IsNew { get; set; } + public string ClassName { get; set; } + public string ObjectId { get; set; } + public DateTime? UpdatedAt { get; set; } + public DateTime? CreatedAt { get; set; } + + public IDictionary ServerData { get; set; } = new Dictionary { }; + + public object this[string key] => ServerData[key]; + + public bool ContainsKey(string key) => ServerData.ContainsKey(key); + + public void Apply(IDictionary operationSet) + { + // Apply operationSet + foreach (KeyValuePair pair in operationSet) + { + ServerData.TryGetValue(pair.Key, out object oldValue); + object newValue = pair.Value.Apply(oldValue, pair.Key); + if (newValue != ParseDeleteOperation.Token) + ServerData[pair.Key] = newValue; + else + ServerData.Remove(pair.Key); + } + } + + public void Apply(IObjectState other) + { + IsNew = other.IsNew; + if (other.ObjectId != null) + ObjectId = other.ObjectId; + if (other.UpdatedAt != null) + UpdatedAt = other.UpdatedAt; + if (other.CreatedAt != null) + CreatedAt = other.CreatedAt; + + foreach (KeyValuePair pair in other) + ServerData[pair.Key] = pair.Value; + } + + public IObjectState MutatedClone(Action func) + { + MutableObjectState clone = MutableClone(); + func(clone); + return clone; + } + + protected virtual MutableObjectState MutableClone() => new MutableObjectState + { + IsNew = IsNew, + ClassName = ClassName, + ObjectId = ObjectId, + CreatedAt = CreatedAt, + UpdatedAt = UpdatedAt, + ServerData = this.ToDictionary(t => t.Key, t => t.Value) + }; + + IEnumerator> IEnumerable>.GetEnumerator() => ServerData.GetEnumerator(); + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => ((IEnumerable>) this).GetEnumerator(); + } +} diff --git a/Parse/Platform/Objects/ParseObject.cs b/Parse/Platform/Objects/ParseObject.cs new file mode 100644 index 00000000..dea9db3e --- /dev/null +++ b/Parse/Platform/Objects/ParseObject.cs @@ -0,0 +1,1126 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Internal; +using Parse.Infrastructure.Control; +using Parse.Abstractions.Infrastructure.Control; +using Parse.Abstractions.Platform.Objects; +using Parse.Infrastructure.Utilities; +using Parse.Platform.Objects; +using Parse.Infrastructure.Data; + +namespace Parse +{ + /// + /// The ParseObject is a local representation of data that can be saved and + /// retrieved from the Parse cloud. + /// + /// + /// The basic workflow for creating new data is to construct a new ParseObject, + /// use the indexer to fill it with data, and then use SaveAsync() to persist to the + /// database. + /// + /// + /// The basic workflow for accessing existing data is to use a ParseQuery + /// to specify which existing data to retrieve. + /// + /// + public class ParseObject : IEnumerable>, INotifyPropertyChanged + { + internal static string AutoClassName { get; } = "_Automatic"; + + internal static ThreadLocal CreatingPointer { get; } = new ThreadLocal(() => false); + + internal TaskQueue TaskQueue { get; } = new TaskQueue { }; + + /// + /// The instance being targeted. This should generally not be set except when an object is being constructed, as otherwise race conditions may occur. The preferred method to set this property is via calling . + /// + public IServiceHub Services { get; set; } + + /// + /// Constructs a new ParseObject with no data in it. A ParseObject constructed in this way will + /// not have an ObjectId and will not persist to the database until + /// is called. + /// + /// + /// Class names must be alphanumerical plus underscore, and start with a letter. It is recommended + /// to name classes in PascalCase. + /// + /// The className for this ParseObject. + /// The implementation instance to target for any resources. This paramater can be effectively set after construction via . + public ParseObject(string className, IServiceHub serviceHub = default) + { + // We use a ThreadLocal rather than passing a parameter so that createWithoutData can do the + // right thing with subclasses. It's ugly and terrible, but it does provide the development + // experience we generally want, so... yeah. Sorry to whomever has to deal with this in the + // future. I pinky-swear we won't make a habit of this -- you believe me, don't you? + + bool isPointer = CreatingPointer.Value; + CreatingPointer.Value = false; + + if (AutoClassName.Equals(className ?? throw new ArgumentException("You must specify a Parse class name when creating a new ParseObject."))) + { + className = GetType().GetParseClassName(); + } + + // Technically, an exception should be thrown here for when both serviceHub and ParseClient.Instance is null, but it is not possible because ParseObjectClass.Constructor for derived classes is a reference to this constructor, and it will be called with a null serviceHub, then Bind will be called on the constructed object, so this needs to fail softly, unfortunately. + // Services = ... ?? throw new InvalidOperationException("A ParseClient needs to be initialized as the configured default instance before any ParseObjects can be instantiated.") + + if ((Services = serviceHub ?? ParseClient.Instance) is { }) + { + // If this is supposed to be created by a factory but wasn't, throw an exception. + + if (!Services.ClassController.GetClassMatch(className, GetType())) + { + throw new ArgumentException("You must create this type of ParseObject using ParseObject.Create() or the proper subclass."); + } + } + + State = new MutableObjectState { ClassName = className }; + OnPropertyChanged(nameof(ClassName)); + + OperationSetQueue.AddLast(new Dictionary()); + + if (!isPointer) + { + Fetched = true; + IsDirty = true; + + SetDefaultValues(); + } + else + { + IsDirty = false; + Fetched = false; + } + } + + #region ParseObject Creation + + /// + /// Constructor for use in ParseObject subclasses. Subclasses must specify a ParseClassName attribute. Subclasses that do not implement a constructor accepting will need to be bond to an implementation instance via after construction. + /// + protected ParseObject(IServiceHub serviceHub = default) : this(AutoClassName, serviceHub) { } + + /// + /// Attaches the given implementation instance to this or -derived class instance. + /// + /// The serviceHub to use for all operations. + /// The instance which was mutated. + public ParseObject Bind(IServiceHub serviceHub) => (Instance: this, Services = serviceHub).Instance; + + /// + /// Occurs when a property value changes. + /// + public event PropertyChangedEventHandler PropertyChanged + { + add + { + PropertyChangedHandler.Add(value); + } + remove + { + PropertyChangedHandler.Remove(value); + } + } + + /// + /// Gets or sets the ParseACL governing this object. + /// + [ParseFieldName("ACL")] + public ParseACL ACL + { + get => GetProperty(default, nameof(ACL)); + set => SetProperty(value, nameof(ACL)); + } + + /// + /// Gets the class name for the ParseObject. + /// + public string ClassName => State.ClassName; + + /// + /// Gets the first time this object was saved as the server sees it, so that if you create a + /// ParseObject, then wait a while, and then call , the + /// creation time will be the time of the first call rather than + /// the time the object was created locally. + /// + [ParseFieldName("createdAt")] + public DateTime? CreatedAt => State.CreatedAt; + + /// + /// Gets whether the ParseObject has been fetched. + /// + public bool IsDataAvailable + { + get + { + lock (Mutex) + { + return Fetched; + } + } + } + + /// + /// Indicates whether this ParseObject has unsaved changes. + /// + public bool IsDirty + { + get + { + lock (Mutex) + { + return CheckIsDirty(true); + } + } + internal set + { + lock (Mutex) + { + Dirty = value; + OnPropertyChanged(nameof(IsDirty)); + } + } + } + + /// + /// Returns true if this object was created by the Parse server when the + /// object might have already been there (e.g. in the case of a Facebook + /// login) + /// + public bool IsNew + { + get => State.IsNew; + internal set + { + MutateState(mutableClone => mutableClone.IsNew = value); + OnPropertyChanged(nameof(IsNew)); + } + } + + /// + /// Gets a set view of the keys contained in this object. This does not include createdAt, + /// updatedAt, or objectId. It does include things like username and ACL. + /// + public ICollection Keys + { + get + { + lock (Mutex) + { + return EstimatedData.Keys; + } + } + } + + /// + /// Gets or sets the object id. An object id is assigned as soon as an object is + /// saved to the server. The combination of a and an + /// uniquely identifies an object in your application. + /// + [ParseFieldName("objectId")] + public string ObjectId + { + get => State.ObjectId; + set + { + IsDirty = true; + SetObjectIdInternal(value); + } + } + + /// + /// Gets the last time this object was updated as the server sees it, so that if you make changes + /// to a ParseObject, then wait a while, and then call , the updated time + /// will be the time of the call rather than the time the object was + /// changed locally. + /// + [ParseFieldName("updatedAt")] + public DateTime? UpdatedAt => State.UpdatedAt; + + public IDictionary CurrentOperations + { + get + { + lock (Mutex) + { + return OperationSetQueue.Last.Value; + } + } + } + + internal object Mutex { get; } = new object { }; + + public IObjectState State { get; private set; } + + internal bool CanBeSerialized + { + get + { + // This method is only used for batching sets of objects for saveAll + // and when saving children automatically. Since it's only used to + // determine whether or not save should be called on them, it only + // needs to examine their current values, so we use estimatedData. + + lock (Mutex) + { + return Services.CanBeSerializedAsValue(EstimatedData); + } + } + } + + bool Dirty { get; set; } + + internal IDictionary EstimatedData { get; } = new Dictionary { }; + + internal bool Fetched { get; set; } + + bool HasDirtyChildren + { + get + { + lock (Mutex) + { + return FindUnsavedChildren().FirstOrDefault() != null; + } + } + } + + LinkedList> OperationSetQueue { get; } = new LinkedList>(); + + SynchronizedEventHandler PropertyChangedHandler { get; } = new SynchronizedEventHandler(); + + /// + /// Gets or sets a value on the object. It is recommended to name + /// keys in partialCamelCaseLikeThis. + /// + /// The key for the object. Keys must be alphanumeric plus underscore + /// and start with a letter. + /// The property is + /// retrieved and is not found. + /// The value for the key. + virtual public object this[string key] + { + get + { + lock (Mutex) + { + CheckGetAccess(key); + object value = EstimatedData[key]; + + // A relation may be deserialized without a parent or key. Either way, + // make sure it's consistent. + + if (value is ParseRelationBase relation) + { + relation.EnsureParentAndKey(this, key); + } + + return value; + } + } + set + { + lock (Mutex) + { + CheckKeyIsMutable(key); + Set(key, value); + } + } + } + + /// + /// Adds a value for the given key, throwing an Exception if the key + /// already has a value. + /// + /// + /// This allows you to use collection initialization syntax when creating ParseObjects, + /// such as: + /// + /// var obj = new ParseObject("MyType") + /// { + /// {"name", "foo"}, + /// {"count", 10}, + /// {"found", false} + /// }; + /// + /// + /// The key for which a value should be set. + /// The value for the key. + public void Add(string key, object value) + { + lock (Mutex) + { + if (ContainsKey(key)) + { + throw new ArgumentException("Key already exists", key); + } + + this[key] = value; + } + } + + /// + /// Atomically adds objects to the end of the list associated with the given key. + /// + /// The key. + /// The objects to add. + public void AddRangeToList(string key, IEnumerable values) + { + lock (Mutex) + { + CheckKeyIsMutable(key); + PerformOperation(key, new ParseAddOperation(values.Cast())); + } + } + + /// + /// Atomically adds objects to the end of the list associated with the given key, + /// only if they are not already present in the list. The position of the inserts are not + /// guaranteed. + /// + /// The key. + /// The objects to add. + public void AddRangeUniqueToList(string key, IEnumerable values) + { + lock (Mutex) + { + CheckKeyIsMutable(key); + PerformOperation(key, new ParseAddUniqueOperation(values.Cast())); + } + } + + #endregion + + /// + /// Atomically adds an object to the end of the list associated with the given key. + /// + /// The key. + /// The object to add. + public void AddToList(string key, object value) => AddRangeToList(key, new[] { value }); + + /// + /// Atomically adds an object to the end of the list associated with the given key, + /// only if it is not already present in the list. The position of the insert is not + /// guaranteed. + /// + /// The key. + /// The object to add. + public void AddUniqueToList(string key, object value) => AddRangeUniqueToList(key, new object[] { value }); + + /// + /// Returns whether this object has a particular key. + /// + /// The key to check for + public bool ContainsKey(string key) + { + lock (Mutex) + { + return EstimatedData.ContainsKey(key); + } + } + + /// + /// Deletes this object on the server. + /// + /// The cancellation token. + public Task DeleteAsync(CancellationToken cancellationToken = default) => TaskQueue.Enqueue(toAwait => DeleteAsync(toAwait, cancellationToken), cancellationToken); + + /// + /// Gets a value for the key of a particular type. + /// The type to convert the value to. Supported types are + /// ParseObject and its descendents, Parse types such as ParseRelation and ParseGeopoint, + /// primitive types,IList<T>, IDictionary<string, T>, and strings. + /// The key of the element to get. + /// The property is + /// retrieved and is not found. + /// + public T Get(string key) => Conversion.To(this[key]); + + /// + /// Access or create a Relation value for a key. + /// + /// The type of object to create a relation for. + /// The key for the relation field. + /// A ParseRelation for the key. + public ParseRelation GetRelation(string key) where T : ParseObject + { + // All the sanity checking is done when add or remove is called. + + TryGetValue(key, out ParseRelation relation); + return relation ?? new ParseRelation(this, key); + } + + /// + /// A helper function for checking whether two ParseObjects point to + /// the same object in the cloud. + /// + public bool HasSameId(ParseObject other) + { + lock (Mutex) + { + return other is { } && Equals(ClassName, other.ClassName) && Equals(ObjectId, other.ObjectId); + } + } + + #region Atomic Increment + + /// + /// Atomically increments the given key by 1. + /// + /// The key to increment. + public void Increment(string key) => Increment(key, 1); + + /// + /// Atomically increments the given key by the given number. + /// + /// The key to increment. + /// The amount to increment by. + public void Increment(string key, long amount) + { + lock (Mutex) + { + CheckKeyIsMutable(key); + PerformOperation(key, new ParseIncrementOperation(amount)); + } + } + + /// + /// Atomically increments the given key by the given number. + /// + /// The key to increment. + /// The amount to increment by. + public void Increment(string key, double amount) + { + lock (Mutex) + { + CheckKeyIsMutable(key); + PerformOperation(key, new ParseIncrementOperation(amount)); + } + } + + /// + /// Indicates whether key is unsaved for this ParseObject. + /// + /// The key to check for. + /// true if the key has been altered and not saved yet, otherwise + /// false. + public bool IsKeyDirty(string key) + { + lock (Mutex) + { + return CurrentOperations.ContainsKey(key); + } + } + + /// + /// Removes a key from the object's data if it exists. + /// + /// The key to remove. + public virtual void Remove(string key) + { + lock (Mutex) + { + CheckKeyIsMutable(key); + PerformOperation(key, ParseDeleteOperation.Instance); + } + } + + /// + /// Atomically removes all instances of the objects in + /// from the list associated with the given key. + /// + /// The key. + /// The objects to remove. + public void RemoveAllFromList(string key, IEnumerable values) + { + lock (Mutex) + { + CheckKeyIsMutable(key); + PerformOperation(key, new ParseRemoveOperation(values.Cast())); + } + } + + /// + /// Clears any changes to this object made since the last call to . + /// + public void Revert() + { + lock (Mutex) + { + if (CurrentOperations.Count > 0) + { + CurrentOperations.Clear(); + RebuildEstimatedData(); + OnPropertyChanged(nameof(IsDirty)); + } + } + } + + /// + /// Saves this object to the server. + /// + /// The cancellation token. + public Task SaveAsync(CancellationToken cancellationToken = default) => TaskQueue.Enqueue(toAwait => SaveAsync(toAwait, cancellationToken), cancellationToken); + + /// + /// Populates result with the value for the key, if possible. + /// + /// The desired type for the value. + /// The key to retrieve a value for. + /// The value for the given key, converted to the + /// requested type, or null if unsuccessful. + /// true if the lookup and conversion succeeded, otherwise + /// false. + public bool TryGetValue(string key, out T result) + { + lock (Mutex) + { + if (ContainsKey(key)) + { + try + { + T temp = Conversion.To(this[key]); + result = temp; + return true; + } + catch + { + result = default; + return false; + } + } + + result = default; + return false; + } + } + + #endregion + + #region Delete Object + + internal Task DeleteAsync(Task toAwait, CancellationToken cancellationToken) + { + if (ObjectId == null) + { + return Task.FromResult(0); + } + + string sessionToken = Services.GetCurrentSessionToken(); + + return toAwait.OnSuccess(_ => Services.ObjectController.DeleteAsync(State, sessionToken, cancellationToken)).Unwrap().OnSuccess(_ => IsDirty = true); + } + + internal virtual Task FetchAsyncInternal(Task toAwait, CancellationToken cancellationToken) => toAwait.OnSuccess(_ => ObjectId == null ? throw new InvalidOperationException("Cannot refresh an object that hasn't been saved to the server.") : Services.ObjectController.FetchAsync(State, Services.GetCurrentSessionToken(), Services, cancellationToken)).Unwrap().OnSuccess(task => + { + HandleFetchResult(task.Result); + return this; + }); + + #endregion + + #region Fetch Object(s) + + /// + /// Fetches this object with the data from the server. + /// + /// The cancellation token. + internal Task FetchAsyncInternal(CancellationToken cancellationToken) => TaskQueue.Enqueue(toAwait => FetchAsyncInternal(toAwait, cancellationToken), cancellationToken); + + internal Task FetchIfNeededAsyncInternal(Task toAwait, CancellationToken cancellationToken) => !IsDataAvailable ? FetchAsyncInternal(toAwait, cancellationToken) : Task.FromResult(this); + + /// + /// If this ParseObject has not been fetched (i.e. returns + /// false), fetches this object with the data from the server. + /// + /// The cancellation token. + internal Task FetchIfNeededAsyncInternal(CancellationToken cancellationToken) => TaskQueue.Enqueue(toAwait => FetchIfNeededAsyncInternal(toAwait, cancellationToken), cancellationToken); + + internal void HandleFailedSave(IDictionary operationsBeforeSave) + { + lock (Mutex) + { + LinkedListNode> opNode = OperationSetQueue.Find(operationsBeforeSave); + IDictionary nextOperations = opNode.Next.Value; + bool wasDirty = nextOperations.Count > 0; + OperationSetQueue.Remove(opNode); + + // Merge the data from the failed save into the next save. + + foreach (KeyValuePair pair in operationsBeforeSave) + { + IParseFieldOperation operation1 = pair.Value; + + nextOperations.TryGetValue(pair.Key, out IParseFieldOperation operation2); + nextOperations[pair.Key] = operation2 is { } ? operation2.MergeWithPrevious(operation1) : operation1; + } + + if (!wasDirty && nextOperations == CurrentOperations && operationsBeforeSave.Count > 0) + { + OnPropertyChanged(nameof(IsDirty)); + } + } + } + + public virtual void HandleFetchResult(IObjectState serverState) + { + lock (Mutex) + { + MergeFromServer(serverState); + } + } + + internal virtual void HandleSave(IObjectState serverState) + { + lock (Mutex) + { + IDictionary operationsBeforeSave = OperationSetQueue.First.Value; + OperationSetQueue.RemoveFirst(); + + // Merge the data from the save and the data from the server into serverData. + + MutateState(mutableClone => mutableClone.Apply(operationsBeforeSave)); + MergeFromServer(serverState); + } + } + + internal void MergeFromObject(ParseObject other) + { + // If they point to the same instance, we don't need to merge + + lock (Mutex) + { + if (this == other) + { + return; + } + } + + // Clear out any changes on this object. + + if (OperationSetQueue.Count != 1) + { + throw new InvalidOperationException("Attempt to MergeFromObject during save."); + } + + OperationSetQueue.Clear(); + + foreach (IDictionary operationSet in other.OperationSetQueue) + { + OperationSetQueue.AddLast(operationSet.ToDictionary(entry => entry.Key, entry => entry.Value)); + } + + lock (Mutex) + { + State = other.State; + } + + RebuildEstimatedData(); + } + + internal virtual void MergeFromServer(IObjectState serverState) + { + // Make a new serverData with fetched values. + + Dictionary newServerData = serverState.ToDictionary(t => t.Key, t => t.Value); + + lock (Mutex) + { + // Trigger handler based on serverState + + if (serverState.ObjectId != null) + { + // If the objectId is being merged in, consider this object to be fetched. + + Fetched = true; + OnPropertyChanged(nameof(IsDataAvailable)); + } + + if (serverState.UpdatedAt != null) + { + OnPropertyChanged(nameof(UpdatedAt)); + } + + if (serverState.CreatedAt != null) + { + OnPropertyChanged(nameof(CreatedAt)); + } + + // We cache the fetched object because subsequent Save operation might flush the fetched objects into Pointers. + + IDictionary fetchedObject = CollectFetchedObjects(); + + foreach (KeyValuePair pair in serverState) + { + object value = pair.Value; + + if (value is ParseObject) + { + // Resolve fetched object. + + ParseObject entity = value as ParseObject; + + if (fetchedObject.ContainsKey(entity.ObjectId)) + { + value = fetchedObject[entity.ObjectId]; + } + } + newServerData[pair.Key] = value; + } + + IsDirty = false; + MutateState(mutableClone => mutableClone.Apply(serverState.MutatedClone(mutableClone => mutableClone.ServerData = newServerData))); + } + } + + internal void MutateState(Action mutator) + { + lock (Mutex) + { + State = State.MutatedClone(mutator); + + // Refresh the estimated data. + + RebuildEstimatedData(); + } + } + + /// + /// Override to run validations on key/value pairs. Make sure to still + /// call the base version. + /// + internal virtual void OnSettingValue(ref string key, ref object value) + { + if (key is null) + { + throw new ArgumentNullException(nameof(key)); + } + } + + /// + /// PerformOperation is like setting a value at an index, but instead of + /// just taking a new value, it takes a ParseFieldOperation that modifies the value. + /// + internal void PerformOperation(string key, IParseFieldOperation operation) + { + lock (Mutex) + { + EstimatedData.TryGetValue(key, out object oldValue); + object newValue = operation.Apply(oldValue, key); + + if (newValue != ParseDeleteOperation.Token) + { + EstimatedData[key] = newValue; + } + else + { + EstimatedData.Remove(key); + } + + bool wasDirty = CurrentOperations.Count > 0; + CurrentOperations.TryGetValue(key, out IParseFieldOperation oldOperation); + IParseFieldOperation newOperation = operation.MergeWithPrevious(oldOperation); + CurrentOperations[key] = newOperation; + + if (!wasDirty) + { + OnPropertyChanged(nameof(IsDirty)); + } + + OnFieldsChanged(new[] { key }); + } + } + + /// + /// Regenerates the estimatedData map from the serverData and operations. + /// + internal void RebuildEstimatedData() + { + lock (Mutex) + { + EstimatedData.Clear(); + + foreach (KeyValuePair item in State) + { + EstimatedData.Add(item); + } + foreach (IDictionary operations in OperationSetQueue) + { + ApplyOperations(operations, EstimatedData); + } + + // We've just applied a bunch of operations to estimatedData which + // may have changed all of its keys. Notify of all keys and properties + // mapped to keys being changed. + + OnFieldsChanged(default); + } + } + + public IDictionary ServerDataToJSONObjectForSerialization() => PointerOrLocalIdEncoder.Instance.Encode(State.ToDictionary(pair => pair.Key, pair => pair.Value), Services) as IDictionary; + + /// + /// Perform Set internally which is not gated by mutability check. + /// + /// key for the object. + /// the value for the key. + public void Set(string key, object value) + { + lock (Mutex) + { + OnSettingValue(ref key, ref value); + + if (!ParseDataEncoder.Validate(value)) + { + throw new ArgumentException("Invalid type for value: " + value.GetType().ToString()); + } + + PerformOperation(key, new ParseSetOperation(value)); + } + } + + /// + /// Allows subclasses to set values for non-pointer construction. + /// + internal virtual void SetDefaultValues() { } + + public void SetIfDifferent(string key, T value) + { + bool hasCurrent = TryGetValue(key, out T current); + + if (value == null) + { + if (hasCurrent) + { + PerformOperation(key, ParseDeleteOperation.Instance); + } + return; + } + + if (!hasCurrent || !value.Equals(current)) + { + Set(key, value); + } + } + + #endregion + + #region Save Object(s) + + /// + /// Pushes new operations onto the queue and returns the current set of operations. + /// + internal IDictionary StartSave() + { + lock (Mutex) + { + IDictionary currentOperations = CurrentOperations; + OperationSetQueue.AddLast(new Dictionary()); + OnPropertyChanged(nameof(IsDirty)); + return currentOperations; + } + } + + #endregion + + /// + /// Gets the value of a property based upon its associated ParseFieldName attribute. + /// + /// The value of the property. + /// The name of the property. + /// The return type of the property. + protected T GetProperty([CallerMemberName] string propertyName = null) => GetProperty(default(T), propertyName); + + /// + /// Gets the value of a property based upon its associated ParseFieldName attribute. + /// + /// The value of the property. + /// The value to return if the property is not present on the ParseObject. + /// The name of the property. + /// The return type of the property. + protected T GetProperty(T defaultValue, [CallerMemberName] string propertyName = null) => TryGetValue(Services.GetFieldForPropertyName(ClassName, propertyName), out T result) ? result : defaultValue; + + /// + /// Gets a relation for a property based upon its associated ParseFieldName attribute. + /// + /// The ParseRelation for the property. + /// The name of the property. + /// The ParseObject subclass type of the ParseRelation. + protected ParseRelation GetRelationProperty([CallerMemberName] string propertyName = null) where T : ParseObject => GetRelation(Services.GetFieldForPropertyName(ClassName, propertyName)); + + protected virtual bool CheckKeyMutable(string key) => true; + + /// + /// Raises change notifications for all properties associated with the given + /// field names. If fieldNames is null, this will notify for all known field-linked + /// properties (e.g. this happens when we recalculate all estimated data from scratch) + /// + protected void OnFieldsChanged(IEnumerable fields) + { + IDictionary mappings = Services.ClassController.GetPropertyMappings(ClassName); + + foreach (string property in mappings is { } ? fields is { } ? from mapping in mappings join field in fields on mapping.Value equals field select mapping.Key : mappings.Keys : Enumerable.Empty()) + { + OnPropertyChanged(property); + } + + OnPropertyChanged("Item[]"); + } + + /// + /// Raises change notifications for a property. Passing null or the empty string + /// notifies the binding framework that all properties/indexes have changed. + /// Passing "Item[]" tells the binding framework that all indexed values + /// have changed (but not all properties) + /// + protected void OnPropertyChanged([CallerMemberName] string propertyName = null) => PropertyChangedHandler.Invoke(this, new PropertyChangedEventArgs(propertyName)); + + protected virtual Task SaveAsync(Task toAwait, CancellationToken cancellationToken) + { + IDictionary currentOperations = null; + + if (!IsDirty) + { + return Task.CompletedTask; + } + + Task deepSaveTask; + string sessionToken; + + lock (Mutex) + { + // Get the JSON representation of the object. + + currentOperations = StartSave(); + sessionToken = Services.GetCurrentSessionToken(); + deepSaveTask = Services.DeepSaveAsync(EstimatedData, sessionToken, cancellationToken); + } + + return deepSaveTask.OnSuccess(_ => toAwait).Unwrap().OnSuccess(_ => Services.ObjectController.SaveAsync(State, currentOperations, sessionToken, Services, cancellationToken)).Unwrap().ContinueWith(task => + { + if (task.IsFaulted || task.IsCanceled) + { + HandleFailedSave(currentOperations); + } + else + { + HandleSave(task.Result); + } + + return task; + }).Unwrap(); + } + + /// + /// Sets the value of a property based upon its associated ParseFieldName attribute. + /// + /// The new value. + /// The name of the property. + /// The type for the property. + protected void SetProperty(T value, [CallerMemberName] string propertyName = null) => this[Services.GetFieldForPropertyName(ClassName, propertyName)] = value; + + void ApplyOperations(IDictionary operations, IDictionary map) + { + lock (Mutex) + { + foreach (KeyValuePair pair in operations) + { + map.TryGetValue(pair.Key, out object oldValue); + object newValue = pair.Value.Apply(oldValue, pair.Key); + + if (newValue != ParseDeleteOperation.Token) + { + map[pair.Key] = newValue; + } + else + { + map.Remove(pair.Key); + } + } + } + } + + void CheckGetAccess(string key) + { + lock (Mutex) + { + if (!CheckIsDataAvailable(key)) + { + throw new InvalidOperationException("ParseObject has no data for this key. Call FetchIfNeededAsync() to get the data."); + } + } + } + + bool CheckIsDataAvailable(string key) + { + lock (Mutex) + { + return IsDataAvailable || EstimatedData.ContainsKey(key); + } + } + + internal bool CheckIsDirty(bool considerChildren) + { + lock (Mutex) + { + return Dirty || CurrentOperations.Count > 0 || considerChildren && HasDirtyChildren; + } + } + + void CheckKeyIsMutable(string key) + { + if (!CheckKeyMutable(key)) + { + throw new InvalidOperationException($@"Cannot change the ""{key}"" property of a ""{ClassName}"" object."); + } + } + + /// + /// Deep traversal of this object to grab a copy of any object referenced by this object. + /// These instances may have already been fetched, and we don't want to lose their data when + /// refreshing or saving. + /// + /// Map of objectId to ParseObject which have been fetched. + IDictionary CollectFetchedObjects() => Services.TraverseObjectDeep(EstimatedData).OfType().Where(o => o.ObjectId != null && o.IsDataAvailable).GroupBy(o => o.ObjectId).ToDictionary(group => group.Key, group => group.Last()); + + IEnumerable FindUnsavedChildren() => Services.TraverseObjectDeep(EstimatedData).OfType().Where(o => o.IsDirty); + + IEnumerator> IEnumerable>.GetEnumerator() + { + lock (Mutex) + { + return EstimatedData.GetEnumerator(); + } + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + lock (Mutex) + { + return ((IEnumerable>) this).GetEnumerator(); + } + } + /// + /// Sets the objectId without marking dirty. + /// + /// The new objectId + void SetObjectIdInternal(string objectId) + { + lock (Mutex) + { + MutateState(mutableClone => mutableClone.ObjectId = objectId); + OnPropertyChanged(nameof(ObjectId)); + } + } + } +} diff --git a/Parse/Platform/Objects/ParseObjectClass.cs b/Parse/Platform/Objects/ParseObjectClass.cs new file mode 100644 index 00000000..c7137fa0 --- /dev/null +++ b/Parse/Platform/Objects/ParseObjectClass.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Parse.Abstractions.Internal; +using Parse.Infrastructure.Utilities; + +namespace Parse.Platform.Objects +{ + internal class ParseObjectClass + { + public ParseObjectClass(Type type, ConstructorInfo constructor) + { + TypeInfo = type.GetTypeInfo(); + DeclaredName = TypeInfo.GetParseClassName(); + Constructor = Constructor = constructor; + PropertyMappings = type.GetProperties().Select(property => (Property: property, FieldNameAttribute: property.GetCustomAttribute(true))).Where(set => set.FieldNameAttribute is { }).ToDictionary(set => set.Property.Name, set => set.FieldNameAttribute.FieldName); + } + + public TypeInfo TypeInfo { get; } + + public string DeclaredName { get; } + + public IDictionary PropertyMappings { get; } + + public ParseObject Instantiate() => Constructor.Invoke(default) as ParseObject; + + ConstructorInfo Constructor { get; } + } +} diff --git a/Parse/Platform/Objects/ParseObjectClassController.cs b/Parse/Platform/Objects/ParseObjectClassController.cs new file mode 100644 index 00000000..13cf1b15 --- /dev/null +++ b/Parse/Platform/Objects/ParseObjectClassController.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Platform.Objects; +using Parse.Infrastructure.Utilities; + +namespace Parse.Platform.Objects +{ + internal class ParseObjectClassController : IParseObjectClassController + { + // Class names starting with _ are documented to be reserved. Use this one here to allow us to "inherit" certain properties. + static string ReservedParseObjectClassName { get; } = "_ParseObject"; + + ReaderWriterLockSlim Mutex { get; } = new ReaderWriterLockSlim { }; + + IDictionary Classes { get; } = new Dictionary { }; + + Dictionary RegisterActions { get; set; } = new Dictionary { }; + + public ParseObjectClassController() => AddValid(typeof(ParseObject)); + + public string GetClassName(Type type) => type == typeof(ParseObject) ? ReservedParseObjectClassName : type.GetParseClassName(); + + public Type GetType(string className) + { + Mutex.EnterReadLock(); + Classes.TryGetValue(className, out ParseObjectClass info); + Mutex.ExitReadLock(); + + return info?.TypeInfo.AsType(); + } + + public bool GetClassMatch(string className, Type type) + { + Mutex.EnterReadLock(); + Classes.TryGetValue(className, out ParseObjectClass subclassInfo); + Mutex.ExitReadLock(); + + return subclassInfo is { } ? subclassInfo.TypeInfo == type.GetTypeInfo() : type == typeof(ParseObject); + } + + public void AddValid(Type type) + { + TypeInfo typeInfo = type.GetTypeInfo(); + + if (!typeof(ParseObject).GetTypeInfo().IsAssignableFrom(typeInfo)) + throw new ArgumentException("Cannot register a type that is not a subclass of ParseObject"); + + string className = GetClassName(type); + + try + { + // Perform this as a single independent transaction, so we can never get into an + // intermediate state where we *theoretically* register the wrong class due to a + // TOCTTOU bug. + + Mutex.EnterWriteLock(); + + if (Classes.TryGetValue(className, out ParseObjectClass previousInfo)) + if (typeInfo.IsAssignableFrom(previousInfo.TypeInfo)) + // Previous subclass is more specific or equal to the current type, do nothing. + + return; + else if (previousInfo.TypeInfo.IsAssignableFrom(typeInfo)) + { + // Previous subclass is parent of new child, fallthrough and actually register this class. + /* Do nothing */ + } + else + throw new ArgumentException($"Tried to register both {previousInfo.TypeInfo.FullName} and {typeInfo.FullName} as the ParseObject subclass of {className}. Cannot determine the right class to use because neither inherits from the other."); + +#warning Constructor detection may erroneously find a constructor which should not be used. + + ConstructorInfo constructor = type.FindConstructor() ?? type.FindConstructor(typeof(string), typeof(IServiceHub)); + + if (constructor is null) + throw new ArgumentException("Cannot register a type that does not implement the default constructor!"); + + Classes[className] = new ParseObjectClass(type, constructor); + } + finally + { + Mutex.ExitWriteLock(); + } + + Mutex.EnterReadLock(); + RegisterActions.TryGetValue(className, out Action toPerform); + Mutex.ExitReadLock(); + + toPerform?.Invoke(); + } + + public void RemoveClass(Type type) + { + Mutex.EnterWriteLock(); + Classes.Remove(GetClassName(type)); + Mutex.ExitWriteLock(); + } + + public void AddRegisterHook(Type type, Action action) + { + Mutex.EnterWriteLock(); + RegisterActions.Add(GetClassName(type), action); + Mutex.ExitWriteLock(); + } + + public ParseObject Instantiate(string className, IServiceHub serviceHub) + { + Mutex.EnterReadLock(); + Classes.TryGetValue(className, out ParseObjectClass info); + Mutex.ExitReadLock(); + + return info is { } ? info.Instantiate().Bind(serviceHub) : new ParseObject(className, serviceHub); + } + + public IDictionary GetPropertyMappings(string className) + { + Mutex.EnterReadLock(); + Classes.TryGetValue(className, out ParseObjectClass info); + + if (info is null) + Classes.TryGetValue(ReservedParseObjectClassName, out info); + + Mutex.ExitReadLock(); + return info.PropertyMappings; + } + + bool SDKClassesAdded { get; set; } + + // ALTERNATE NAME: AddObject, AddType, AcknowledgeType, CatalogType + + public void AddIntrinsic() + { + if (!(SDKClassesAdded, SDKClassesAdded = true).SDKClassesAdded) + { + AddValid(typeof(ParseUser)); + AddValid(typeof(ParseRole)); + AddValid(typeof(ParseSession)); + AddValid(typeof(ParseInstallation)); + } + } + } +} diff --git a/Parse/Platform/Objects/ParseObjectController.cs b/Parse/Platform/Objects/ParseObjectController.cs new file mode 100644 index 00000000..62363e3f --- /dev/null +++ b/Parse/Platform/Objects/ParseObjectController.cs @@ -0,0 +1,148 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure.Control; +using Parse.Abstractions.Infrastructure.Data; +using Parse.Abstractions.Infrastructure.Execution; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Platform.Objects; +using Parse.Infrastructure.Utilities; +using Parse.Infrastructure; +using Parse.Abstractions.Internal; +using Parse.Infrastructure.Execution; +using Parse.Infrastructure.Data; + +namespace Parse.Platform.Objects +{ + public class ParseObjectController : IParseObjectController + { + IParseCommandRunner CommandRunner { get; } + + IParseDataDecoder Decoder { get; } + + IServerConnectionData ServerConnectionData { get; } + + public ParseObjectController(IParseCommandRunner commandRunner, IParseDataDecoder decoder, IServerConnectionData serverConnectionData) => (CommandRunner, Decoder, ServerConnectionData) = (commandRunner, decoder, serverConnectionData); + + public Task FetchAsync(IObjectState state, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default) + { + ParseCommand command = new ParseCommand($"classes/{Uri.EscapeDataString(state.ClassName)}/{Uri.EscapeDataString(state.ObjectId)}", method: "GET", sessionToken: sessionToken, data: default); + return CommandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(task => ParseObjectCoder.Instance.Decode(task.Result.Item2, Decoder, serviceHub)); + } + + public Task SaveAsync(IObjectState state, IDictionary operations, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default) + { + ParseCommand command = new ParseCommand(state.ObjectId == null ? $"classes/{Uri.EscapeDataString(state.ClassName)}" : $"classes/{Uri.EscapeDataString(state.ClassName)}/{state.ObjectId}", method: state.ObjectId is null ? "POST" : "PUT", sessionToken: sessionToken, data: serviceHub.GenerateJSONObjectForSaving(operations)); + return CommandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).OnSuccess(task => ParseObjectCoder.Instance.Decode(task.Result.Item2, Decoder, serviceHub).MutatedClone(mutableClone => mutableClone.IsNew = task.Result.Item1 == System.Net.HttpStatusCode.Created)); + } + + public IList> SaveAllAsync(IList states, IList> operationsList, string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default) => ExecuteBatchRequests(states.Zip(operationsList, (item, operations) => new ParseCommand(item.ObjectId is null ? $"classes/{Uri.EscapeDataString(item.ClassName)}" : $"classes/{Uri.EscapeDataString(item.ClassName)}/{Uri.EscapeDataString(item.ObjectId)}", method: item.ObjectId is null ? "POST" : "PUT", data: serviceHub.GenerateJSONObjectForSaving(operations))).ToList(), sessionToken, cancellationToken).Select(task => task.OnSuccess(task => ParseObjectCoder.Instance.Decode(task.Result, Decoder, serviceHub))).ToList(); + + public Task DeleteAsync(IObjectState state, string sessionToken, CancellationToken cancellationToken = default) => CommandRunner.RunCommandAsync(new ParseCommand($"classes/{state.ClassName}/{state.ObjectId}", method: "DELETE", sessionToken: sessionToken, data: null), cancellationToken: cancellationToken); + + public IList DeleteAllAsync(IList states, string sessionToken, CancellationToken cancellationToken = default) => ExecuteBatchRequests(states.Where(item => item.ObjectId is { }).Select(item => new ParseCommand($"classes/{Uri.EscapeDataString(item.ClassName)}/{Uri.EscapeDataString(item.ObjectId)}", method: "DELETE", data: default)).ToList(), sessionToken, cancellationToken).Cast().ToList(); + + int MaximumBatchSize { get; } = 50; + + // TODO (hallucinogen): move this out to a class to be used by Analytics + + internal IList>> ExecuteBatchRequests(IList requests, string sessionToken, CancellationToken cancellationToken = default) + { + List>> tasks = new List>>(); + int batchSize = requests.Count; + + IEnumerable remaining = requests; + + while (batchSize > MaximumBatchSize) + { + List process = remaining.Take(MaximumBatchSize).ToList(); + + remaining = remaining.Skip(MaximumBatchSize); + tasks.AddRange(ExecuteBatchRequest(process, sessionToken, cancellationToken)); + batchSize = remaining.Count(); + } + + tasks.AddRange(ExecuteBatchRequest(remaining.ToList(), sessionToken, cancellationToken)); + return tasks; + } + + IList>> ExecuteBatchRequest(IList requests, string sessionToken, CancellationToken cancellationToken = default) + { + int batchSize = requests.Count; + + List>> tasks = new List>> { }; + List>> completionSources = new List>> { }; + + for (int i = 0; i < batchSize; ++i) + { + TaskCompletionSource> tcs = new TaskCompletionSource>(); + + completionSources.Add(tcs); + tasks.Add(tcs.Task); + } + + List encodedRequests = requests.Select(request => + { + Dictionary results = new Dictionary + { + ["method"] = request.Method, + ["path"] = request is { Path: { }, Resource: { } } ? request.Target.AbsolutePath : new Uri(new Uri(ServerConnectionData.ServerURI), request.Path).AbsolutePath, + }; + + if (request.DataObject != null) + results["body"] = request.DataObject; + + return results; + }).Cast().ToList(); + + ParseCommand command = new ParseCommand("batch", method: "POST", sessionToken: sessionToken, data: new Dictionary { [nameof(requests)] = encodedRequests }); + + CommandRunner.RunCommandAsync(command, cancellationToken: cancellationToken).ContinueWith(task => + { + if (task.IsFaulted || task.IsCanceled) + { + foreach (TaskCompletionSource> tcs in completionSources) + if (task.IsFaulted) + tcs.TrySetException(task.Exception); + else if (task.IsCanceled) + tcs.TrySetCanceled(); + + return; + } + + IList resultsArray = Conversion.As>(task.Result.Item2["results"]); + int resultLength = resultsArray.Count; + + if (resultLength != batchSize) + { + foreach (TaskCompletionSource> completionSource in completionSources) + completionSource.TrySetException(new InvalidOperationException($"Batch command result count expected: {batchSize} but was: {resultLength}.")); + + return; + } + + for (int i = 0; i < batchSize; ++i) + { + Dictionary result = resultsArray[i] as Dictionary; + TaskCompletionSource> target = completionSources[i]; + + if (result.ContainsKey("success")) + target.TrySetResult(result["success"] as IDictionary); + else if (result.ContainsKey("error")) + { + IDictionary error = result["error"] as IDictionary; + target.TrySetException(new ParseFailureException((ParseFailureException.ErrorCode) (long) error["code"], error[nameof(error)] as string)); + } + else + target.TrySetException(new InvalidOperationException("Invalid batch command response.")); + } + }); + + return tasks; + } + } +} diff --git a/Parse/Platform/ParseClient.cs b/Parse/Platform/ParseClient.cs new file mode 100644 index 00000000..68535c72 --- /dev/null +++ b/Parse/Platform/ParseClient.cs @@ -0,0 +1,153 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Parse.Abstractions.Infrastructure; +using Parse.Infrastructure.Utilities; +using Parse.Infrastructure; + +#if DEBUG +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Parse.Tests")] +#endif + +namespace Parse +{ + /// + /// ParseClient contains static functions that handle global + /// configuration for the Parse library. + /// + public class ParseClient : CustomServiceHub, IServiceHubComposer + { + /// + /// Contains, in order, the official ISO date and time format strings, and two modified versions that account for the possibility that the server-side string processing mechanism removed trailing zeroes. + /// + internal static string[] DateFormatStrings { get; } = + { + "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'", + "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'ff'Z'", + "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'f'Z'", + }; + + /// + /// Gets whether or not the assembly using the Parse SDK was compiled by IL2CPP. + /// + public static bool IL2CPPCompiled { get; set; } = AppDomain.CurrentDomain?.FriendlyName?.Equals("IL2CPP Root Domain") == true; + + /// + /// The configured default instance of to use. + /// + public static ParseClient Instance { get; private set; } + + internal static string Version => typeof(ParseClient)?.Assembly?.GetCustomAttribute()?.InformationalVersion ?? typeof(ParseClient)?.Assembly?.GetName()?.Version?.ToString(); + + /// + /// Services that provide essential functionality. + /// + public override IServiceHub Services { get; internal set; } + + // TODO: Implement IServiceHubMutator in all IServiceHub-implementing classes in Parse.Library and possibly require all implementations to do so as an efficiency improvement over instantiating an OrchestrationServiceHub, only for another one to be possibly instantiated when configurators are specified. + + /// + /// Creates a new and authenticates it as belonging to your application. This class is a hub for interacting with the SDK. The recommended way to use this class on client applications is to instantiate it, then call on it in your application entry point. This allows you to access . + /// + /// The Application ID provided in the Parse dashboard. + /// The server URI provided in the Parse dashboard. + /// The .NET Key provided in the Parse dashboard. + /// A service hub to override internal services and thereby make the Parse SDK operate in a custom manner. + /// A set of implementation instances to tweak the behaviour of the SDK. + public ParseClient(string applicationID, string serverURI, string key, IServiceHub serviceHub = default, params IServiceHubMutator[] configurators) : this(new ServerConnectionData { ApplicationID = applicationID, ServerURI = serverURI, Key = key }, serviceHub, configurators) { } + + /// + /// Creates a new and authenticates it as belonging to your application. This class is a hub for interacting with the SDK. The recommended way to use this class on client applications is to instantiate it, then call on it in your application entry point. This allows you to access . + /// + /// The configuration to initialize Parse with. + /// A service hub to override internal services and thereby make the Parse SDK operate in a custom manner. + /// A set of implementation instances to tweak the behaviour of the SDK. + public ParseClient(IServerConnectionData configuration, IServiceHub serviceHub = default, params IServiceHubMutator[] configurators) + { + Services = serviceHub is { } ? new OrchestrationServiceHub { Custom = serviceHub, Default = new ServiceHub { ServerConnectionData = GenerateServerConnectionData() } } : new ServiceHub { ServerConnectionData = GenerateServerConnectionData() } as IServiceHub; + + IServerConnectionData GenerateServerConnectionData() => configuration switch + { + null => throw new ArgumentNullException(nameof(configuration)), + ServerConnectionData { Test: true, ServerURI: { } } data => data, + ServerConnectionData { Test: true } data => new ServerConnectionData + { + ApplicationID = data.ApplicationID, + Headers = data.Headers, + MasterKey = data.MasterKey, + Test = data.Test, + Key = data.Key, + ServerURI = "https://api.parse.com/1/" + }, + { ServerURI: "https://api.parse.com/1/" } => throw new InvalidOperationException("Since the official parse server has shut down, you must specify a URI that points to a hosted instance."), + { ApplicationID: { }, ServerURI: { }, Key: { } } data => data, + _ => throw new InvalidOperationException("The IServerConnectionData implementation instance provided to the ParseClient constructor must be populated with the information needed to connect to a Parse server instance.") + }; + + if (configurators is { Length: int length } && length > 0) + { + Services = serviceHub switch + { + IMutableServiceHub { } mutableServiceHub => BuildHub((Hub: mutableServiceHub, mutableServiceHub.ServerConnectionData = serviceHub.ServerConnectionData ?? Services.ServerConnectionData).Hub, Services, configurators), + { } => BuildHub(default, Services, configurators) + }; + } + + Services.ClassController.AddIntrinsic(); + } + + /// + /// Initializes a instance using the set on the 's implementation instance. + /// + public ParseClient() => Services = (Instance ?? throw new InvalidOperationException("A ParseClient instance with an initializer service must first be publicized in order for the default constructor to be used.")).Services.Cloner.BuildHub(Instance.Services, this); + + /// + /// Sets this instance as the template to create new instances from. + /// + ///// Declares that the current instance should be the publicly-accesible . + public void Publicize() + { + lock (Mutex) + { + Instance = this; + } + } + + static object Mutex { get; } = new object { }; + + internal static string BuildQueryString(IDictionary parameters) => String.Join("&", (from pair in parameters let valueString = pair.Value as string select $"{Uri.EscapeDataString(pair.Key)}={Uri.EscapeDataString(String.IsNullOrEmpty(valueString) ? JsonUtilities.Encode(pair.Value) : valueString)}").ToArray()); + + internal static IDictionary DecodeQueryString(string queryString) + { + Dictionary query = new Dictionary { }; + + foreach (string pair in queryString.Split('&')) + { + string[] parts = pair.Split(new char[] { '=' }, 2); + query[parts[0]] = parts.Length == 2 ? Uri.UnescapeDataString(parts[1].Replace("+", " ")) : null; + } + + return query; + } + + internal static IDictionary DeserializeJsonString(string jsonData) => JsonUtilities.Parse(jsonData) as IDictionary; + + internal static string SerializeJsonString(IDictionary jsonData) => JsonUtilities.Encode(jsonData); + + public IServiceHub BuildHub(IMutableServiceHub target = default, IServiceHub extension = default, params IServiceHubMutator[] configurators) + { + OrchestrationServiceHub orchestrationServiceHub = new OrchestrationServiceHub { Custom = target ??= new MutableServiceHub { }, Default = extension ?? new ServiceHub { } }; + + foreach (IServiceHubMutator mutator in configurators.Where(configurator => configurator.Valid)) + { + mutator.Mutate(ref target, orchestrationServiceHub); + orchestrationServiceHub.Custom = target; + } + + return orchestrationServiceHub; + } + } +} diff --git a/Parse/Platform/Push/MutablePushState.cs b/Parse/Platform/Push/MutablePushState.cs new file mode 100644 index 00000000..0b942b4f --- /dev/null +++ b/Parse/Platform/Push/MutablePushState.cs @@ -0,0 +1,57 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections.Generic; +using Parse.Abstractions.Platform.Push; +using Parse.Infrastructure.Utilities; + +namespace Parse.Platform.Push +{ + public class MutablePushState : IPushState + { + public ParseQuery Query { get; set; } + public IEnumerable Channels { get; set; } + public DateTime? Expiration { get; set; } + public TimeSpan? ExpirationInterval { get; set; } + public DateTime? PushTime { get; set; } + public IDictionary Data { get; set; } + public string Alert { get; set; } + + public IPushState MutatedClone(Action func) + { + MutablePushState clone = MutableClone(); + func(clone); + return clone; + } + + protected virtual MutablePushState MutableClone() => new MutablePushState + { + Query = Query, + Channels = Channels == null ? null : new List(Channels), + Expiration = Expiration, + ExpirationInterval = ExpirationInterval, + PushTime = PushTime, + Data = Data == null ? null : new Dictionary(Data), + Alert = Alert + }; + + public override bool Equals(object obj) + { + if (obj == null || !(obj is MutablePushState)) + return false; + + MutablePushState other = obj as MutablePushState; + return Equals(Query, other.Query) && + Channels.CollectionsEqual(other.Channels) && + Equals(Expiration, other.Expiration) && + Equals(ExpirationInterval, other.ExpirationInterval) && + Equals(PushTime, other.PushTime) && + Data.CollectionsEqual(other.Data) && + Equals(Alert, other.Alert); + } + + public override int GetHashCode() => + // TODO (richardross): Implement this. + 0; + } +} diff --git a/Parse/Platform/Push/ParsePush.cs b/Parse/Platform/Push/ParsePush.cs new file mode 100644 index 00000000..3c8f57d6 --- /dev/null +++ b/Parse/Platform/Push/ParsePush.cs @@ -0,0 +1,216 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Platform.Push; +using Parse.Platform.Push; + +namespace Parse +{ + /// + /// A utility class for sending and receiving push notifications. + /// + public partial class ParsePush + { + object Mutex { get; } = new object { }; + + IPushState State { get; set; } + + IServiceHub Services { get; } + +#warning Make default(IServiceHub) the default value of serviceHub once all dependents properly inject it. + + /// + /// Creates a push which will target every device. The Data field must be set before calling SendAsync. + /// + public ParsePush(IServiceHub serviceHub) + { + Services = serviceHub ?? ParseClient.Instance; + State = new MutablePushState { Query = Services.GetInstallationQuery() }; + } + + #region Properties + + /// + /// An installation query that specifies which installations should receive + /// this push. + /// This should not be used in tandem with Channels. + /// + public ParseQuery Query + { + get => State.Query; + set => MutateState(state => + { + if (state.Channels is { } && value is { } && value.GetConstraint("channels") is { }) + { + throw new InvalidOperationException("A push may not have both Channels and a Query with a channels constraint."); + } + + state.Query = value; + }); + } + + /// + /// A short-hand to set a query which only discriminates on the channels to which a device is subscribed. + /// This is shorthand for: + /// + /// + /// var push = new Push(); + /// push.Query = ParseInstallation.Query.WhereKeyContainedIn("channels", channels); + /// + /// + /// This cannot be used in tandem with Query. + /// + public IEnumerable Channels + { + get => State.Channels; + set => MutateState(state => + { + if (value is { } && state.Query is { } && state.Query.GetConstraint("channels") is { }) + { + throw new InvalidOperationException("A push may not have both Channels and a Query with a channels constraint."); + } + + state.Channels = value; + }); + } + + /// + /// The time at which this push will expire. This should not be used in tandem with ExpirationInterval. + /// + public DateTime? Expiration + { + get => State.Expiration; + set => MutateState(state => + { + if (state.ExpirationInterval is { }) + { + throw new InvalidOperationException("Cannot set Expiration after setting ExpirationInterval."); + } + + state.Expiration = value; + }); + } + + /// + /// The time at which this push will be sent. + /// + public DateTime? PushTime + { + get => State.PushTime; + set => MutateState(state => + { + DateTime now = DateTime.Now; + + if (value < now || value > now.AddDays(14)) + { + throw new InvalidOperationException("Cannot set PushTime in the past or more than two weeks later than now."); + } + + state.PushTime = value; + }); + } + + /// + /// The time from initial schedul when this push will expire. This should not be used in tandem with Expiration. + /// + public TimeSpan? ExpirationInterval + { + get => State.ExpirationInterval; + set => MutateState(state => + { + if (state.Expiration is { }) + { + throw new InvalidOperationException("Cannot set ExpirationInterval after setting Expiration."); + } + + state.ExpirationInterval = value; + }); + } + + /// + /// The contents of this push. Some keys have special meaning. A full list of pre-defined + /// keys can be found in the Parse Push Guide. The following keys affect WinRT devices. + /// Keys which do not start with x-winrt- can be prefixed with x-winrt- to specify an + /// override only sent to winrt devices. + /// alert: the body of the alert text. + /// title: The title of the text. + /// x-winrt-payload: A full XML payload to be sent to WinRT installations instead of + /// the auto-layout. + /// This should not be used in tandem with Alert. + /// + public IDictionary Data + { + get => State.Data; + set => MutateState(state => + { + if (state.Alert is { } && value is { }) + { + throw new InvalidOperationException("A push may not have both an Alert and Data."); + } + + state.Data = value; + }); + } + + /// + /// A convenience method which sets Data to a dictionary with alert as its only field. Equivalent to + /// + /// + /// Data = new Dictionary<string, object> {{"alert", alert}}; + /// + /// + /// This should not be used in tandem with Data. + /// + public string Alert + { + get => State.Alert; + set => MutateState(state => + { + if (state.Data is { } && value is { }) + { + throw new InvalidOperationException("A push may not have both an Alert and Data."); + } + + state.Alert = value; + }); + } + + #endregion + + internal IDictionary Encode() => ParsePushEncoder.Instance.Encode(State); + + void MutateState(Action func) + { + lock (Mutex) + { + State = State.MutatedClone(func); + } + } + + #region Sending Push + + /// + /// Request a push to be sent. When this task completes, Parse has successfully acknowledged a request + /// to send push notifications but has not necessarily finished sending all notifications + /// requested. The current status of recent push notifications can be seen in your Push Notifications + /// console on http://parse.com + /// + /// A Task for continuation. + public Task SendAsync() => SendAsync(CancellationToken.None); + + /// + /// Request a push to be sent. When this task completes, Parse has successfully acknowledged a request + /// to send push notifications but has not necessarily finished sending all notifications + /// requested. The current status of recent push notifications can be seen in your Push Notifications + /// console on http://parse.com + /// + /// CancellationToken to cancel the current operation. + public Task SendAsync(CancellationToken cancellationToken) => Services.PushController.SendPushNotificationAsync(State, Services, cancellationToken); + + #endregion + } +} diff --git a/Parse/Platform/Push/ParsePushChannelsController.cs b/Parse/Platform/Push/ParsePushChannelsController.cs new file mode 100644 index 00000000..58377658 --- /dev/null +++ b/Parse/Platform/Push/ParsePushChannelsController.cs @@ -0,0 +1,32 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Parse; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Platform.Installations; +using Parse.Abstractions.Platform.Push; +using Parse.Infrastructure.Utilities; + +namespace Parse.Platform.Push +{ + internal class ParsePushChannelsController : IParsePushChannelsController + { + IParseCurrentInstallationController CurrentInstallationController { get; } + + public ParsePushChannelsController(IParseCurrentInstallationController currentInstallationController) => CurrentInstallationController = currentInstallationController; + + public Task SubscribeAsync(IEnumerable channels, IServiceHub serviceHub, CancellationToken cancellationToken = default) => CurrentInstallationController.GetAsync(serviceHub, cancellationToken).OnSuccess(task => + { + task.Result.AddRangeUniqueToList(nameof(channels), channels); + return task.Result.SaveAsync(cancellationToken); + }).Unwrap(); + + public Task UnsubscribeAsync(IEnumerable channels, IServiceHub serviceHub, CancellationToken cancellationToken = default) => CurrentInstallationController.GetAsync(serviceHub, cancellationToken).OnSuccess(task => + { + task.Result.RemoveAllFromList(nameof(channels), channels); + return task.Result.SaveAsync(cancellationToken); + }).Unwrap(); + } +} diff --git a/Parse/Platform/Push/ParsePushController.cs b/Parse/Platform/Push/ParsePushController.cs new file mode 100644 index 00000000..52b912b4 --- /dev/null +++ b/Parse/Platform/Push/ParsePushController.cs @@ -0,0 +1,28 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Execution; +using Parse.Abstractions.Platform.Push; +using Parse.Abstractions.Platform.Users; +using Parse.Infrastructure.Execution; +using Parse.Infrastructure.Utilities; + +namespace Parse.Platform.Push +{ + internal class ParsePushController : IParsePushController + { + IParseCommandRunner CommandRunner { get; } + + IParseCurrentUserController CurrentUserController { get; } + + public ParsePushController(IParseCommandRunner commandRunner, IParseCurrentUserController currentUserController) + { + CommandRunner = commandRunner; + CurrentUserController = currentUserController; + } + + public Task SendPushNotificationAsync(IPushState state, IServiceHub serviceHub, CancellationToken cancellationToken = default) => CurrentUserController.GetCurrentSessionTokenAsync(serviceHub, cancellationToken).OnSuccess(sessionTokenTask => CommandRunner.RunCommandAsync(new ParseCommand("push", method: "POST", sessionToken: sessionTokenTask.Result, data: ParsePushEncoder.Instance.Encode(state)), cancellationToken: cancellationToken)).Unwrap(); + } +} diff --git a/Parse/Internal/Push/Coder/ParsePushEncoder.cs b/Parse/Platform/Push/ParsePushEncoder.cs similarity index 58% rename from Parse/Internal/Push/Coder/ParsePushEncoder.cs rename to Parse/Platform/Push/ParsePushEncoder.cs index f010ae41..a739df75 100644 --- a/Parse/Internal/Push/Coder/ParsePushEncoder.cs +++ b/Parse/Platform/Push/ParsePushEncoder.cs @@ -1,59 +1,51 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. using System; -using System.Linq; using System.Collections.Generic; -using Parse.Common.Internal; -using Parse.Core.Internal; +using Parse.Abstractions.Platform.Push; +using Parse.Infrastructure.Utilities; -namespace Parse.Push.Internal +namespace Parse.Platform.Push { public class ParsePushEncoder { - private static readonly ParsePushEncoder instance = new ParsePushEncoder(); - public static ParsePushEncoder Instance - { - get - { - return instance; - } - } + public static ParsePushEncoder Instance { get; } = new ParsePushEncoder { }; private ParsePushEncoder() { } public IDictionary Encode(IPushState state) { - if (state.Alert == null && state.Data == null) - { + if (state.Alert is null && state.Data is null) throw new InvalidOperationException("A push must have either an Alert or Data"); - } - if (state.Channels == null && state.Query == null) - { + + if (state.Channels is null && state.Query is null) throw new InvalidOperationException("A push must have either Channels or a Query"); - } - var data = state.Data ?? new Dictionary { { "alert", state.Alert } }; - var query = state.Query ?? ParseInstallation.Query; - if (state.Channels != null) + IDictionary data = state.Data ?? new Dictionary { + ["alert"] = state.Alert + }; + +#warning Verify that it is fine to instantiate a ParseQuery here with a default(IServiceHub). + + ParseQuery query = state.Query ?? new ParseQuery(default, "_Installation") { }; + + if (state.Channels != null) query = query.WhereContainedIn("channels", state.Channels); - } - var payload = new Dictionary { - { "data", data }, - { "where", query.BuildParameters().GetOrDefault("where", new Dictionary()) }, - }; - if (state.Expiration.HasValue) + + Dictionary payload = new Dictionary { + [nameof(data)] = data, + ["where"] = query.BuildParameters().GetOrDefault("where", new Dictionary { }), + }; + + if (state.Expiration.HasValue) payload["expiration_time"] = state.Expiration.Value.ToString("yyyy-MM-ddTHH:mm:ssZ"); - } else if (state.ExpirationInterval.HasValue) - { payload["expiration_interval"] = state.ExpirationInterval.Value.TotalSeconds; - } + if (state.PushTime.HasValue) - { payload["push_time"] = state.PushTime.Value.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"); - } return payload; } diff --git a/Parse/Public/ParsePushNotificationEventArgs.cs b/Parse/Platform/Push/ParsePushNotificationEvent.cs similarity index 60% rename from Parse/Public/ParsePushNotificationEventArgs.cs rename to Parse/Platform/Push/ParsePushNotificationEvent.cs index a4fdd065..6e82c6bb 100644 --- a/Parse/Public/ParsePushNotificationEventArgs.cs +++ b/Parse/Platform/Push/ParsePushNotificationEvent.cs @@ -1,45 +1,40 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. -using Parse.Common.Internal; using System; using System.Collections.Generic; +using Parse.Infrastructure.Utilities; -namespace Parse +namespace Parse.Platform.Push { /// /// A wrapper around Parse push notification payload. /// - public class ParsePushNotificationEventArgs : EventArgs + public class ParsePushNotificationEvent : EventArgs { - internal ParsePushNotificationEventArgs(IDictionary payload) + internal ParsePushNotificationEvent(IDictionary content) { - Payload = payload; - -#if !IOS - StringPayload = Json.Encode(payload); -#endif + Content = content; + TextContent = JsonUtilities.Encode(content); } // TODO: (richardross) investigate this. // Obj-C type -> .NET type is impossible to do flawlessly (especially // on NSNumber). We can't transform NSDictionary into string because of this reason. -#if !IOS - internal ParsePushNotificationEventArgs(string stringPayload) - { - StringPayload = stringPayload; - Payload = Json.Parse(stringPayload) as IDictionary; + internal ParsePushNotificationEvent(string stringPayload) + { + TextContent = stringPayload; + Content = JsonUtilities.Parse(stringPayload) as IDictionary; } -#endif /// /// The payload of the push notification as IDictionary. /// - public IDictionary Payload { get; internal set; } + public IDictionary Content { get; internal set; } /// /// The payload of the push notification as string. /// - public string StringPayload { get; internal set; } + public string TextContent { get; internal set; } } } diff --git a/Parse/Public/ParseQuery.cs b/Parse/Platform/Queries/ParseQuery.cs similarity index 74% rename from Parse/Public/ParseQuery.cs rename to Parse/Platform/Queries/ParseQuery.cs index 7ea296a1..babad75d 100644 --- a/Parse/Public/ParseQuery.cs +++ b/Parse/Platform/Queries/ParseQuery.cs @@ -1,16 +1,17 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. using System; -using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using Parse.Core.Internal; -using Parse.Common.Internal; -using System.Net.WebSockets; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Platform.Objects; +using Parse.Infrastructure; +using Parse.Infrastructure.Data; +using Parse.Infrastructure.Utilities; namespace Parse { @@ -29,7 +30,7 @@ namespace Parse /// /// /// A ParseQuery can also be used to retrieve a single object whose id is known, - /// through the method. For example, this sample code + /// through the method. For example, this sample code /// fetches an object of class "MyClass" and id myId. /// /// @@ -48,106 +49,145 @@ namespace Parse /// public class ParseQuery where T : ParseObject { - private readonly string className; - private readonly Dictionary where; - private readonly ReadOnlyCollection orderBy; - private readonly ReadOnlyCollection includes; - private readonly ReadOnlyCollection selectedKeys; - private readonly String redirectClassNameForKey; - private readonly int? skip; - private readonly int? limit; + /// + /// Serialized clauses. + /// + Dictionary Filters { get; } + + /// + /// Serialized clauses. + /// + ReadOnlyCollection Orderings { get; } + + /// + /// Serialized related data query merging request (data inclusion) clauses. + /// + ReadOnlyCollection Includes { get; } + + /// + /// Serialized key selections. + /// + ReadOnlyCollection KeySelections { get; } + + string RedirectClassNameForKey { get; } + + int? SkipAmount { get; } - internal string ClassName => className; + int? LimitAmount { get; } - internal static IParseQueryController QueryController => ParseCorePlugins.Instance.QueryController; + internal string ClassName { get; } - internal static IObjectSubclassingController SubclassingController => ParseCorePlugins.Instance.SubclassingController; + internal IServiceHub Services { get; } /// /// Private constructor for composition of queries. A source query is required, /// but the remaining values can be null if they won't be changed in this /// composition. /// - private ParseQuery(ParseQuery source, IDictionary where = null, IEnumerable replacementOrderBy = null, IEnumerable thenBy = null, int? skip = null, int? limit = null, IEnumerable includes = null, IEnumerable selectedKeys = null, String redirectClassNameForKey = null) + internal ParseQuery(ParseQuery source, IDictionary where = null, IEnumerable replacementOrderBy = null, IEnumerable thenBy = null, int? skip = null, int? limit = null, IEnumerable includes = null, IEnumerable selectedKeys = null, string redirectClassNameForKey = null) { if (source == null) - throw new ArgumentNullException("source"); - - className = source.className; - this.where = source.where; - orderBy = replacementOrderBy is null ? source.orderBy : new ReadOnlyCollection(replacementOrderBy.ToList()); - this.skip = skip is null ? source.skip : (source.skip ?? 0) + skip; // 0 could be handled differently from null - this.limit = limit ?? source.limit; - this.includes = source.includes; - this.selectedKeys = source.selectedKeys; - this.redirectClassNameForKey = redirectClassNameForKey ?? source.redirectClassNameForKey; - - if (thenBy != null) { - List newOrderBy = new List(orderBy ?? throw new ArgumentException("You must call OrderBy before calling ThenBy.")); + throw new ArgumentNullException(nameof(source)); + } + + Services = source.Services; + ClassName = source.ClassName; + Filters = source.Filters; + Orderings = replacementOrderBy is null ? source.Orderings : new ReadOnlyCollection(replacementOrderBy.ToList()); + + // 0 could be handled differently from null. + + SkipAmount = skip is null ? source.SkipAmount : (source.SkipAmount ?? 0) + skip; + LimitAmount = limit ?? source.LimitAmount; + Includes = source.Includes; + KeySelections = source.KeySelections; + RedirectClassNameForKey = redirectClassNameForKey ?? source.RedirectClassNameForKey; + + if (thenBy is { }) + { + List newOrderBy = new List(Orderings ?? throw new ArgumentException("You must call OrderBy before calling ThenBy.")); newOrderBy.AddRange(thenBy); - orderBy = new ReadOnlyCollection(newOrderBy); + Orderings = new ReadOnlyCollection(newOrderBy); } - + // Remove duplicates. - if (orderBy != null) - orderBy = new ReadOnlyCollection(new HashSet(orderBy).ToList()); - if (where != null) - this.where = new Dictionary(MergeWhereClauses(where)); + if (Orderings is { }) + { + Orderings = new ReadOnlyCollection(new HashSet(Orderings).ToList()); + } - if (includes != null) - this.includes = new ReadOnlyCollection(MergeIncludes(includes).ToList()); + if (where is { }) + { + Filters = new Dictionary(MergeWhereClauses(where)); + } - if (selectedKeys != null) - this.selectedKeys = new ReadOnlyCollection(MergeSelectedKeys(selectedKeys).ToList()); + if (includes is { }) + { + Includes = new ReadOnlyCollection(MergeIncludes(includes).ToList()); + } + + if (selectedKeys is { }) + { + KeySelections = new ReadOnlyCollection(MergeSelectedKeys(selectedKeys).ToList()); + } } - private HashSet MergeIncludes(IEnumerable includes) + HashSet MergeIncludes(IEnumerable includes) { - if (this.includes == null) + if (Includes is null) + { return new HashSet(includes); - HashSet newIncludes = new HashSet(this.includes); + } + + HashSet newIncludes = new HashSet(Includes); + foreach (string item in includes) + { newIncludes.Add(item); + } + return newIncludes; } - private HashSet MergeSelectedKeys(IEnumerable selectedKeys) - { - if (this.selectedKeys == null) - return new HashSet(selectedKeys); - HashSet newSelectedKeys = new HashSet(this.selectedKeys); - foreach (string item in selectedKeys) - newSelectedKeys.Add(item); - return newSelectedKeys; - } + HashSet MergeSelectedKeys(IEnumerable selectedKeys) => new HashSet((KeySelections ?? Enumerable.Empty()).Concat(selectedKeys)); - private IDictionary MergeWhereClauses(IDictionary where) + IDictionary MergeWhereClauses(IDictionary where) { - if (this.where == null) + if (Filters is null) + { return where; - var newWhere = new Dictionary(this.where); - foreach (var pair in where) + } + + Dictionary newWhere = new Dictionary(Filters); + foreach (KeyValuePair pair in where) { - var condition = pair.Value as IDictionary; if (newWhere.ContainsKey(pair.Key)) { - var oldCondition = newWhere[pair.Key] as IDictionary; - if (oldCondition == null || condition == null) + if (!(newWhere[pair.Key] is IDictionary oldCondition) || !(pair.Value is IDictionary condition)) + { throw new ArgumentException("More than one where clause for the given key provided."); - var newCondition = new Dictionary(oldCondition); - foreach (var conditionPair in condition) + } + + Dictionary newCondition = new Dictionary(oldCondition); + foreach (KeyValuePair conditionPair in condition) { if (newCondition.ContainsKey(conditionPair.Key)) + { throw new ArgumentException("More than one condition for the given key provided."); + } + newCondition[conditionPair.Key] = conditionPair.Value; } + newWhere[pair.Key] = newCondition; } else + { newWhere[pair.Key] = pair.Value; + } } return newWhere; } @@ -155,41 +195,14 @@ private IDictionary MergeWhereClauses(IDictionary /// Constructs a query based upon the ParseObject subclass used as the generic parameter for the ParseQuery. /// - public ParseQuery() : this(SubclassingController.GetClassName(typeof(T))) { } + public ParseQuery(IServiceHub serviceHub) : this(serviceHub, serviceHub.ClassController.GetClassName(typeof(T))) { } /// /// Constructs a query. A default query with no further parameters will retrieve /// all s of the provided class. /// /// The name of the class to retrieve ParseObjects for. - public ParseQuery(string className) => this.className = className ?? throw new ArgumentNullException("className", "Must specify a ParseObject class name when creating a ParseQuery."); - - /// - /// Constructs a query that is the or of the given queries. - /// - /// The list of ParseQueries to 'or' together. - /// A ParseQquery that is the 'or' of the passed in queries. - public static ParseQuery Or(IEnumerable> queries) - { - string className = null; - var orValue = new List>(); - // We need to cast it to non-generic IEnumerable because of AOT-limitation - var nonGenericQueries = (IEnumerable) queries; - foreach (var obj in nonGenericQueries) - { - var q = obj as ParseQuery; - if (className != null && q.className != className) - throw new ArgumentException("All of the queries in an or query must be on the same class."); - className = q.className; - var parameters = q.BuildParameters(); - if (parameters.Count == 0) - continue; - if (!parameters.TryGetValue("where", out object where) || parameters.Count > 1) - throw new ArgumentException("None of the queries in an or query can have non-filtering clauses"); - orValue.Add(where as IDictionary); - } - return new ParseQuery(new ParseQuery(className), where: new Dictionary { { "$or", orValue } }); - } + public ParseQuery(IServiceHub serviceHub, string className) => (ClassName, Services) = (className ?? throw new ArgumentNullException(nameof(className), "Must specify a ParseObject class name when creating a ParseQuery."), serviceHub); #region Order By @@ -270,7 +283,7 @@ public static ParseQuery Or(IEnumerable> queries) /// A new query with the additional constraint. public ParseQuery Limit(int count) => new ParseQuery(this, limit: count); - internal ParseQuery RedirectClassName(String key) => new ParseQuery(this, redirectClassNameForKey: key); + internal ParseQuery RedirectClassName(string key) => new ParseQuery(this, redirectClassNameForKey: key); #region Where @@ -431,7 +444,17 @@ public static ParseQuery Or(IEnumerable> queries) /// The key in the objects from the subquery to look in. /// The subquery to run /// A new query with the additional constraint. - public ParseQuery WhereMatchesKeyInQuery(string key, string keyInQuery, ParseQuery query) where TOther : ParseObject => new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$select", new Dictionary { { "query", query.BuildParameters(true) }, { "key", keyInQuery } } } } } }); + public ParseQuery WhereMatchesKeyInQuery(string key, string keyInQuery, ParseQuery query) where TOther : ParseObject => new ParseQuery(this, where: new Dictionary + { + [key] = new Dictionary + { + ["$select"] = new Dictionary + { + [nameof(query)] = query.BuildParameters(true), + [nameof(key)] = keyInQuery + } + } + }); /// /// Adds a constraint to the query that requires a particular key's value @@ -441,7 +464,17 @@ public static ParseQuery Or(IEnumerable> queries) /// The key in the objects from the subquery to look in. /// The subquery to run /// A new query with the additional constraint. - public ParseQuery WhereDoesNotMatchesKeyInQuery(string key, string keyInQuery, ParseQuery query) where TOther : ParseObject => new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$dontSelect", new Dictionary { { "query", query.BuildParameters(true) }, { "key", keyInQuery } } } } } }); + public ParseQuery WhereDoesNotMatchesKeyInQuery(string key, string keyInQuery, ParseQuery query) where TOther : ParseObject => new ParseQuery(this, where: new Dictionary + { + [key] = new Dictionary + { + ["$dontSelect"] = new Dictionary + { + [nameof(query)] = query.BuildParameters(true), + [nameof(key)] = keyInQuery + } + } + }); /// /// Adds a constraint to the query that requires that a particular key's value @@ -451,7 +484,13 @@ public static ParseQuery Or(IEnumerable> queries) /// The key to check. /// The query that the value should match. /// A new query with the additional constraint. - public ParseQuery WhereMatchesQuery(string key, ParseQuery query) where TOther : ParseObject => new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$inQuery", query.BuildParameters(true) } } } }); + public ParseQuery WhereMatchesQuery(string key, ParseQuery query) where TOther : ParseObject => new ParseQuery(this, where: new Dictionary + { + [key] = new Dictionary + { + ["$inQuery"] = query.BuildParameters(true) + } + }); /// /// Adds a proximity-based constraint for finding objects with keys whose GeoPoint @@ -460,7 +499,13 @@ public static ParseQuery Or(IEnumerable> queries) /// The key that the ParseGeoPoint is stored in. /// The reference ParseGeoPoint. /// A new query with the additional constraint. - public ParseQuery WhereNear(string key, ParseGeoPoint point) => new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$nearSphere", point } } } }); + public ParseQuery WhereNear(string key, ParseGeoPoint point) => new ParseQuery(this, where: new Dictionary + { + [key] = new Dictionary + { + ["$nearSphere"] = point + } + }); /// /// Adds a constraint to the query that requires a particular key's value to be @@ -469,7 +514,13 @@ public static ParseQuery Or(IEnumerable> queries) /// The key to check. /// The values that will match. /// A new query with the additional constraint. - public ParseQuery WhereNotContainedIn(string key, IEnumerable values) => new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$nin", values.ToList() } } } }); + public ParseQuery WhereNotContainedIn(string key, IEnumerable values) => new ParseQuery(this, where: new Dictionary + { + [key] = new Dictionary + { + ["$nin"] = values.ToList() + } + }); /// /// Adds a constraint to the query that requires a particular key's value not @@ -478,7 +529,13 @@ public static ParseQuery Or(IEnumerable> queries) /// The key to check. /// The value that that must not be equalled. /// A new query with the additional constraint. - public ParseQuery WhereNotEqualTo(string key, object value) => new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$ne", value } } } }); + public ParseQuery WhereNotEqualTo(string key, object value) => new ParseQuery(this, where: new Dictionary + { + [key] = new Dictionary + { + ["$ne"] = value + } + }); /// /// Adds a constraint for finding string values that start with the provided string. @@ -487,7 +544,13 @@ public static ParseQuery Or(IEnumerable> queries) /// The key that the string to match is stored in. /// The substring that the value must start with. /// A new query with the additional constraint. - public ParseQuery WhereStartsWith(string key, string suffix) => new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$regex", "^" + RegexQuote(suffix) } } } }); + public ParseQuery WhereStartsWith(string key, string suffix) => new ParseQuery(this, where: new Dictionary + { + [key] = new Dictionary + { + ["$regex"] = $"^{RegexQuote(suffix)}" + } + }); /// /// Add a constraint to the query that requires a particular key's coordinates to be @@ -497,7 +560,20 @@ public static ParseQuery Or(IEnumerable> queries) /// The lower-left inclusive corner of the box. /// The upper-right inclusive corner of the box. /// A new query with the additional constraint. - public ParseQuery WhereWithinGeoBox(string key, ParseGeoPoint southwest, ParseGeoPoint northeast) => new ParseQuery(this, where: new Dictionary { { key, new Dictionary { { "$within", new Dictionary { { "$box", new[] { southwest, northeast } } } } } } }); + public ParseQuery WhereWithinGeoBox(string key, ParseGeoPoint southwest, ParseGeoPoint northeast) => new ParseQuery(this, where: new Dictionary + { + [key] = new Dictionary + { + ["$within"] = new Dictionary + { + ["$box"] = new[] + { + southwest, + northeast + } + } + } + }); /// /// Adds a proximity-based constraint for finding objects with keys whose GeoPoint @@ -507,9 +583,22 @@ public static ParseQuery Or(IEnumerable> queries) /// The reference ParseGeoPoint. /// The maximum distance (in radians) of results to return. /// A new query with the additional constraint. - public ParseQuery WhereWithinDistance(string key, ParseGeoPoint point, ParseGeoDistance maxDistance) => new ParseQuery(WhereNear(key, point), where: new Dictionary { { key, new Dictionary { { "$maxDistance", maxDistance.Radians } } } }); + public ParseQuery WhereWithinDistance(string key, ParseGeoPoint point, ParseGeoDistance maxDistance) => new ParseQuery(WhereNear(key, point), where: new Dictionary + { + [key] = new Dictionary + { + ["$maxDistance"] = maxDistance.Radians + } + }); - internal ParseQuery WhereRelatedTo(ParseObject parent, string key) => new ParseQuery(this, where: new Dictionary { { "$relatedTo", new Dictionary { { "object", parent }, { "key", key } } } }); + internal ParseQuery WhereRelatedTo(ParseObject parent, string key) => new ParseQuery(this, where: new Dictionary + { + ["$relatedTo"] = new Dictionary + { + ["object"] = parent, + [nameof(key)] = key + } + }); #endregion @@ -527,7 +616,7 @@ public static ParseQuery Or(IEnumerable> queries) public Task> FindAsync(CancellationToken cancellationToken) { EnsureNotInstallationQuery(); - return QueryController.FindAsync(this, ParseUser.CurrentUser, cancellationToken).OnSuccess(t => from state in t.Result select ParseObject.FromState(state, ClassName)); + return Services.QueryController.FindAsync(this, Services.GetCurrentUser(), cancellationToken).OnSuccess(task => from state in task.Result select Services.GenerateObjectFromState(state, ClassName)); } /// @@ -544,14 +633,14 @@ public Task> FindAsync(CancellationToken cancellationToken) public Task FirstOrDefaultAsync(CancellationToken cancellationToken) { EnsureNotInstallationQuery(); - return QueryController.FirstAsync(this, ParseUser.CurrentUser, cancellationToken).OnSuccess(t => t.Result is IObjectState state && state != null ? ParseObject.FromState(state, ClassName) : default(T)); + return Services.QueryController.FirstAsync(this, Services.GetCurrentUser(), cancellationToken).OnSuccess(task => task.Result is IObjectState state && state is { } ? Services.GenerateObjectFromState(state, ClassName) : default); } /// /// Retrieves at most one ParseObject that satisfies this query. /// /// A single ParseObject that satisfies this query. - /// If no results match the query. + /// If no results match the query. public Task FirstAsync() => FirstAsync(CancellationToken.None); /// @@ -559,8 +648,8 @@ public Task FirstOrDefaultAsync(CancellationToken cancellationToken) /// /// The cancellation token. /// A single ParseObject that satisfies this query. - /// If no results match the query. - public Task FirstAsync(CancellationToken cancellationToken) => FirstOrDefaultAsync(cancellationToken).OnSuccess(t => t.Result ?? throw new ParseException(ParseException.ErrorCode.ObjectNotFound, "No results matched the query.")); + /// If no results match the query. + public Task FirstAsync(CancellationToken cancellationToken) => FirstOrDefaultAsync(cancellationToken).OnSuccess(task => task.Result ?? throw new ParseFailureException(ParseFailureException.ErrorCode.ObjectNotFound, "No results matched the query.")); /// /// Counts the number of objects that match this query. @@ -576,7 +665,7 @@ public Task FirstOrDefaultAsync(CancellationToken cancellationToken) public Task CountAsync(CancellationToken cancellationToken) { EnsureNotInstallationQuery(); - return QueryController.CountAsync(this, ParseUser.CurrentUser, cancellationToken); + return Services.QueryController.CountAsync(this, Services.GetCurrentUser(), cancellationToken); } /// @@ -596,38 +685,38 @@ public Task CountAsync(CancellationToken cancellationToken) /// The ParseObject for the given objectId. public Task GetAsync(string objectId, CancellationToken cancellationToken) { - ParseQuery singleItemQuery = new ParseQuery(className).WhereEqualTo("objectId", objectId); - singleItemQuery = new ParseQuery(singleItemQuery, includes: includes, selectedKeys: selectedKeys, limit: 1); - return singleItemQuery.FindAsync(cancellationToken).OnSuccess(t => t.Result.FirstOrDefault() ?? throw new ParseException(ParseException.ErrorCode.ObjectNotFound, "Object with the given objectId not found.")); + ParseQuery singleItemQuery = new ParseQuery(Services, ClassName).WhereEqualTo(nameof(objectId), objectId); + singleItemQuery = new ParseQuery(singleItemQuery, includes: Includes, selectedKeys: KeySelections, limit: 1); + return singleItemQuery.FindAsync(cancellationToken).OnSuccess(t => t.Result.FirstOrDefault() ?? throw new ParseFailureException(ParseFailureException.ErrorCode.ObjectNotFound, "Object with the given objectId not found.")); } - internal object GetConstraint(string key) => where?.GetOrDefault(key, null); + internal object GetConstraint(string key) => Filters?.GetOrDefault(key, null); internal IDictionary BuildParameters(bool includeClassName = false) { Dictionary result = new Dictionary(); - if (where != null) - result["where"] = PointerOrLocalIdEncoder.Instance.Encode(where); - if (orderBy != null) - result["order"] = string.Join(",", orderBy.ToArray()); - if (skip != null) - result["skip"] = skip.Value; - if (limit != null) - result["limit"] = limit.Value; - if (includes != null) - result["include"] = string.Join(",", includes.ToArray()); - if (selectedKeys != null) - result["keys"] = string.Join(",", selectedKeys.ToArray()); + if (Filters != null) + result["where"] = PointerOrLocalIdEncoder.Instance.Encode(Filters, Services); + if (Orderings != null) + result["order"] = String.Join(",", Orderings.ToArray()); + if (SkipAmount != null) + result["skip"] = SkipAmount.Value; + if (LimitAmount != null) + result["limit"] = LimitAmount.Value; + if (Includes != null) + result["include"] = String.Join(",", Includes.ToArray()); + if (KeySelections != null) + result["keys"] = String.Join(",", KeySelections.ToArray()); if (includeClassName) - result["className"] = className; - if (redirectClassNameForKey != null) - result["redirectClassNameForKey"] = redirectClassNameForKey; + result["className"] = ClassName; + if (RedirectClassNameForKey != null) + result["redirectClassNameForKey"] = RedirectClassNameForKey; return result; } - private string RegexQuote(string input) => "\\Q" + input.Replace("\\E", "\\E\\\\E\\Q") + "\\E"; + string RegexQuote(string input) => "\\Q" + input.Replace("\\E", "\\E\\\\E\\Q") + "\\E"; - private string GetRegexOptions(Regex regex, string modifiers) + string GetRegexOptions(Regex regex, string modifiers) { string result = modifiers ?? ""; if (regex.Options.HasFlag(RegexOptions.IgnoreCase) && !modifiers.Contains("i")) @@ -637,20 +726,27 @@ private string GetRegexOptions(Regex regex, string modifiers) return result; } - private IDictionary EncodeRegex(Regex regex, string modifiers) + IDictionary EncodeRegex(Regex regex, string modifiers) { - var options = GetRegexOptions(regex, modifiers); - var dict = new Dictionary { ["$regex"] = regex.ToString() }; - if (!string.IsNullOrEmpty(options)) + string options = GetRegexOptions(regex, modifiers); + Dictionary dict = new Dictionary { ["$regex"] = regex.ToString() }; + + if (!String.IsNullOrEmpty(options)) + { dict["$options"] = options; + } + return dict; } - private void EnsureNotInstallationQuery() + void EnsureNotInstallationQuery() { // The ParseInstallation class is not accessible from this project; using string literal. - if (className.Equals("_Installation")) + + if (ClassName.Equals("_Installation")) + { throw new InvalidOperationException("Cannot directly query the Installation class."); + } } /// @@ -658,16 +754,14 @@ private void EnsureNotInstallationQuery() /// /// The object to compare with the current object. /// true if the specified object is equal to the current object; otherwise, false - public override bool Equals(object obj) => obj == null || !(obj is ParseQuery other) ? false : Equals(className, other.ClassName) && where.CollectionsEqual(other.where) && orderBy.CollectionsEqual(other.orderBy) && includes.CollectionsEqual(other.includes) && selectedKeys.CollectionsEqual(other.selectedKeys) && Equals(skip, other.skip) && Equals(limit, other.limit); + public override bool Equals(object obj) => obj == null || !(obj is ParseQuery other) ? false : Equals(ClassName, other.ClassName) && Filters.CollectionsEqual(other.Filters) && Orderings.CollectionsEqual(other.Orderings) && Includes.CollectionsEqual(other.Includes) && KeySelections.CollectionsEqual(other.KeySelections) && Equals(SkipAmount, other.SkipAmount) && Equals(LimitAmount, other.LimitAmount); /// /// Serves as the default hash function. /// /// A hash code for the current object. - public override int GetHashCode() - { + public override int GetHashCode() => // TODO (richardross): Implement this. - return 0; - } + 0; } } diff --git a/Parse/Platform/Queries/ParseQueryController.cs b/Parse/Platform/Queries/ParseQueryController.cs new file mode 100644 index 00000000..cc5d775c --- /dev/null +++ b/Parse/Platform/Queries/ParseQueryController.cs @@ -0,0 +1,50 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure.Data; +using Parse.Abstractions.Infrastructure.Execution; +using Parse.Abstractions.Platform.Objects; +using Parse.Abstractions.Platform.Queries; +using Parse.Infrastructure.Data; +using Parse.Infrastructure.Execution; +using Parse.Infrastructure.Utilities; + +namespace Parse.Platform.Queries +{ + /// + /// A straightforward implementation of that uses to decode raw server data when needed. + /// + internal class ParseQueryController : IParseQueryController + { + IParseCommandRunner CommandRunner { get; } + + IParseDataDecoder Decoder { get; } + + public ParseQueryController(IParseCommandRunner commandRunner, IParseDataDecoder decoder) => (CommandRunner, Decoder) = (commandRunner, decoder); + + public Task> FindAsync(ParseQuery query, ParseUser user, CancellationToken cancellationToken = default) where T : ParseObject => FindAsync(query.ClassName, query.BuildParameters(), user?.SessionToken, cancellationToken).OnSuccess(t => (from item in t.Result["results"] as IList select ParseObjectCoder.Instance.Decode(item as IDictionary, Decoder, user.Services))); + + public Task CountAsync(ParseQuery query, ParseUser user, CancellationToken cancellationToken = default) where T : ParseObject + { + IDictionary parameters = query.BuildParameters(); + parameters["limit"] = 0; + parameters["count"] = 1; + + return FindAsync(query.ClassName, parameters, user?.SessionToken, cancellationToken).OnSuccess(task => Convert.ToInt32(task.Result["count"])); + } + + public Task FirstAsync(ParseQuery query, ParseUser user, CancellationToken cancellationToken = default) where T : ParseObject + { + IDictionary parameters = query.BuildParameters(); + parameters["limit"] = 1; + + return FindAsync(query.ClassName, parameters, user?.SessionToken, cancellationToken).OnSuccess(task => (task.Result["results"] as IList).FirstOrDefault() as IDictionary is Dictionary item && item != null ? ParseObjectCoder.Instance.Decode(item, Decoder, user.Services) : null); + } + + Task> FindAsync(string className, IDictionary parameters, string sessionToken, CancellationToken cancellationToken = default) => CommandRunner.RunCommandAsync(new ParseCommand($"classes/{Uri.EscapeDataString(className)}?{ParseClient.BuildQueryString(parameters)}", method: "GET", sessionToken: sessionToken, data: null), cancellationToken: cancellationToken).OnSuccess(t => t.Result.Item2); + } +} diff --git a/Parse/Platform/Relations/ParseRelation.cs b/Parse/Platform/Relations/ParseRelation.cs new file mode 100644 index 00000000..d25c5a25 --- /dev/null +++ b/Parse/Platform/Relations/ParseRelation.cs @@ -0,0 +1,108 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq.Expressions; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Platform.Objects; +using Parse.Infrastructure.Control; + +namespace Parse +{ + public static class RelationServiceExtensions + { + /// + /// Produces the proper ParseRelation<T> instance for the given classname. + /// + internal static ParseRelationBase CreateRelation(this IServiceHub serviceHub, ParseObject parent, string key, string targetClassName) => serviceHub.ClassController.CreateRelation(parent, key, targetClassName); + + internal static ParseRelationBase CreateRelation(this IParseObjectClassController classController, ParseObject parent, string key, string targetClassName) + { + Expression>> createRelationExpr = () => CreateRelation(parent, key, targetClassName); + return (createRelationExpr.Body as MethodCallExpression).Method.GetGenericMethodDefinition().MakeGenericMethod(classController.GetType(targetClassName) ?? typeof(ParseObject)).Invoke(default, new object[] { parent, key, targetClassName }) as ParseRelationBase; + } + + static ParseRelation CreateRelation(ParseObject parent, string key, string targetClassName) where T : ParseObject => new ParseRelation(parent, key, targetClassName); + } + + /// + /// A common base class for ParseRelations. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract class ParseRelationBase : IJsonConvertible + { + ParseObject Parent { get; set; } + + string Key { get; set; } + + internal ParseRelationBase(ParseObject parent, string key) => EnsureParentAndKey(parent, key); + + internal ParseRelationBase(ParseObject parent, string key, string targetClassName) : this(parent, key) => TargetClassName = targetClassName; + + internal void EnsureParentAndKey(ParseObject parent, string key) + { + Parent ??= parent; + Key ??= key; + + Debug.Assert(Parent == parent, "Relation retrieved from two different objects"); + Debug.Assert(Key == key, "Relation retrieved from two different keys"); + } + + internal void Add(ParseObject entity) + { + ParseRelationOperation change = new ParseRelationOperation(Parent.Services.ClassController, new[] { entity }, default); + + Parent.PerformOperation(Key, change); + TargetClassName = change.TargetClassName; + } + + internal void Remove(ParseObject entity) + { + ParseRelationOperation change = new ParseRelationOperation(Parent.Services.ClassController, default, new[] { entity }); + + Parent.PerformOperation(Key, change); + TargetClassName = change.TargetClassName; + } + + IDictionary IJsonConvertible.ConvertToJSON() => new Dictionary + { + ["__type"] = "Relation", + ["className"] = TargetClassName + }; + + internal ParseQuery GetQuery() where T : ParseObject => TargetClassName is { } ? new ParseQuery(Parent.Services, TargetClassName).WhereRelatedTo(Parent, Key) : new ParseQuery(Parent.Services, Parent.ClassName).RedirectClassName(Key).WhereRelatedTo(Parent, Key); + + internal string TargetClassName { get; set; } + } + + /// + /// Provides access to all of the children of a many-to-many relationship. Each instance of + /// ParseRelation is associated with a particular parent and key. + /// + /// The type of the child objects. + public sealed class ParseRelation : ParseRelationBase where T : ParseObject + { + internal ParseRelation(ParseObject parent, string key) : base(parent, key) { } + + internal ParseRelation(ParseObject parent, string key, string targetClassName) : base(parent, key, targetClassName) { } + + /// + /// Adds an object to this relation. The object must already have been saved. + /// + /// The object to add. + public void Add(T obj) => base.Add(obj); + + /// + /// Removes an object from this relation. The object must already have been saved. + /// + /// The object to remove. + public void Remove(T obj) => base.Remove(obj); + + /// + /// Gets a query that can be used to query the objects in this relation. + /// + public ParseQuery Query => GetQuery(); + } +} diff --git a/Parse/Public/ParseRole.cs b/Parse/Platform/Roles/ParseRole.cs similarity index 75% rename from Parse/Public/ParseRole.cs rename to Parse/Platform/Roles/ParseRole.cs index 28d2c79a..b978fff1 100644 --- a/Parse/Public/ParseRole.cs +++ b/Parse/Platform/Roles/ParseRole.cs @@ -1,12 +1,7 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. -using Parse.Core.Internal; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; namespace Parse { @@ -35,8 +30,7 @@ public ParseRole() : base() { } /// /// The name of the role to create. /// The ACL for this role. Roles must have an ACL. - public ParseRole(string name, ParseACL acl) - : this() + public ParseRole(string name, ParseACL acl) : this() { Name = name; ACL = acl; @@ -48,8 +42,8 @@ public ParseRole(string name, ParseACL acl) [ParseFieldName("name")] public string Name { - get { return GetProperty("Name"); } - set { SetProperty(value, "Name"); } + get => GetProperty(nameof(Name)); + set => SetProperty(value, nameof(Name)); } /// @@ -59,10 +53,7 @@ public string Name /// add or remove child users from the role through this relation. /// [ParseFieldName("users")] - public ParseRelation Users - { - get { return GetRelationProperty("Users"); } - } + public ParseRelation Users => GetRelationProperty("Users"); /// /// Gets the for the s that are @@ -71,10 +62,7 @@ public ParseRelation Users /// add or remove child roles from the role through this relation. /// [ParseFieldName("roles")] - public ParseRelation Roles - { - get { return GetRelationProperty("Roles"); } - } + public ParseRelation Roles => GetRelationProperty("Roles"); internal override void OnSettingValue(ref string key, ref object value) { @@ -88,26 +76,13 @@ internal override void OnSettingValue(ref string key, ref object value) } if (!(value is string)) { - throw new ArgumentException("A role's name must be a string.", "value"); + throw new ArgumentException("A role's name must be a string.", nameof(value)); } if (!namePattern.IsMatch((string) value)) { - throw new ArgumentException( - "A role's name can only contain alphanumeric characters, _, -, and spaces.", - "value"); + throw new ArgumentException("A role's name can only contain alphanumeric characters, _, -, and spaces.", nameof(value)); } } } - - /// - /// Gets a over the Role collection. - /// - public static ParseQuery Query - { - get - { - return new ParseQuery(); - } - } } } diff --git a/Parse/Public/ParseACL.cs b/Parse/Platform/Security/ParseACL.cs similarity index 98% rename from Parse/Public/ParseACL.cs rename to Parse/Platform/Security/ParseACL.cs index 88c98a4d..432eb721 100644 --- a/Parse/Public/ParseACL.cs +++ b/Parse/Platform/Security/ParseACL.cs @@ -3,8 +3,8 @@ using System; using System.Collections.Generic; using System.Linq; -using Parse.Core.Internal; -using Parse.Common.Internal; +using Parse.Abstractions.Internal; +using Parse.Abstractions.Infrastructure; namespace Parse { @@ -53,7 +53,7 @@ public ParseACL(ParseUser owner) SetWriteAccess(owner, true); } - IDictionary IJsonConvertible.ToJSON() + IDictionary IJsonConvertible.ConvertToJSON() { Dictionary result = new Dictionary(); foreach (string user in readers.Union(writers)) diff --git a/Parse/Platform/Sessions/ParseSession.cs b/Parse/Platform/Sessions/ParseSession.cs new file mode 100644 index 00000000..b53923eb --- /dev/null +++ b/Parse/Platform/Sessions/ParseSession.cs @@ -0,0 +1,23 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System.Collections.Generic; + +namespace Parse +{ + /// + /// Represents a session of a user for a Parse application. + /// + [ParseClassName("_Session")] + public class ParseSession : ParseObject + { + static HashSet ImmutableKeys { get; } = new HashSet { "sessionToken", "createdWith", "restricted", "user", "expiresAt", "installationId" }; + + protected override bool CheckKeyMutable(string key) => !ImmutableKeys.Contains(key); + + /// + /// Gets the session token for a user, if they are logged in. + /// + [ParseFieldName("sessionToken")] + public string SessionToken => GetProperty(default, "SessionToken"); + } +} diff --git a/Parse/Platform/Sessions/ParseSessionController.cs b/Parse/Platform/Sessions/ParseSessionController.cs new file mode 100644 index 00000000..5f729045 --- /dev/null +++ b/Parse/Platform/Sessions/ParseSessionController.cs @@ -0,0 +1,33 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure.Data; +using Parse.Abstractions.Infrastructure.Execution; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Platform.Sessions; +using Parse.Infrastructure.Utilities; +using Parse.Abstractions.Platform.Objects; +using Parse.Infrastructure.Execution; +using Parse.Infrastructure.Data; + +namespace Parse.Platform.Sessions +{ + public class ParseSessionController : IParseSessionController + { + IParseCommandRunner CommandRunner { get; } + + IParseDataDecoder Decoder { get; } + + public ParseSessionController(IParseCommandRunner commandRunner, IParseDataDecoder decoder) => (CommandRunner, Decoder) = (commandRunner, decoder); + + public Task GetSessionAsync(string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default) => CommandRunner.RunCommandAsync(new ParseCommand("sessions/me", method: "GET", sessionToken: sessionToken, data: null), cancellationToken: cancellationToken).OnSuccess(task => ParseObjectCoder.Instance.Decode(task.Result.Item2, Decoder, serviceHub)); + + public Task RevokeAsync(string sessionToken, CancellationToken cancellationToken = default) => CommandRunner.RunCommandAsync(new ParseCommand("logout", method: "POST", sessionToken: sessionToken, data: new Dictionary { }), cancellationToken: cancellationToken); + + public Task UpgradeToRevocableSessionAsync(string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default) => CommandRunner.RunCommandAsync(new ParseCommand("upgradeToRevocableSession", method: "POST", sessionToken: sessionToken, data: new Dictionary()), cancellationToken: cancellationToken).OnSuccess(task => ParseObjectCoder.Instance.Decode(task.Result.Item2, Decoder, serviceHub)); + + public bool IsRevocableSessionToken(string sessionToken) => sessionToken.Contains("r:"); + } +} diff --git a/Parse/Platform/Users/ParseCurrentUserController.cs b/Parse/Platform/Users/ParseCurrentUserController.cs new file mode 100644 index 00000000..680110a5 --- /dev/null +++ b/Parse/Platform/Users/ParseCurrentUserController.cs @@ -0,0 +1,116 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure.Data; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Platform.Objects; +using Parse.Abstractions.Platform.Users; +using Parse.Infrastructure.Utilities; +using Parse.Infrastructure.Data; + +namespace Parse.Platform.Users +{ +#warning This class needs to be rewritten (PCuUsC). + + public class ParseCurrentUserController : IParseCurrentUserController + { + object Mutex { get; } = new object { }; + + TaskQueue TaskQueue { get; } = new TaskQueue { }; + + ICacheController StorageController { get; } + + IParseObjectClassController ClassController { get; } + + IParseDataDecoder Decoder { get; } + + public ParseCurrentUserController(ICacheController storageController, IParseObjectClassController classController, IParseDataDecoder decoder) => (StorageController, ClassController, Decoder) = (storageController, classController, decoder); + + ParseUser currentUser; + public ParseUser CurrentUser + { + get + { + lock (Mutex) + return currentUser; + } + set + { + lock (Mutex) + currentUser = value; + } + } + + public Task SetAsync(ParseUser user, CancellationToken cancellationToken) => TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => + { + Task saveTask = default; + + if (user is null) + saveTask = StorageController.LoadAsync().OnSuccess(task => task.Result.RemoveAsync(nameof(CurrentUser))).Unwrap(); + else + { + // TODO (hallucinogen): we need to use ParseCurrentCoder instead of this janky encoding + + IDictionary data = user.ServerDataToJSONObjectForSerialization(); + data["objectId"] = user.ObjectId; + + if (user.CreatedAt != null) + data["createdAt"] = user.CreatedAt.Value.ToString(ParseClient.DateFormatStrings.First(), CultureInfo.InvariantCulture); + if (user.UpdatedAt != null) + data["updatedAt"] = user.UpdatedAt.Value.ToString(ParseClient.DateFormatStrings.First(), CultureInfo.InvariantCulture); + + saveTask = StorageController.LoadAsync().OnSuccess(task => task.Result.AddAsync(nameof(CurrentUser), JsonUtilities.Encode(data))).Unwrap(); + } + + CurrentUser = user; + return saveTask; + }).Unwrap(), cancellationToken); + + public Task GetAsync(IServiceHub serviceHub, CancellationToken cancellationToken = default) + { + ParseUser cachedCurrent; + + lock (Mutex) + cachedCurrent = CurrentUser; + + return cachedCurrent is { } ? Task.FromResult(cachedCurrent) : TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => StorageController.LoadAsync().OnSuccess(task => + { + task.Result.TryGetValue(nameof(CurrentUser), out object data); + ParseUser user = default; + + if (data is string { } serialization) + user = ClassController.GenerateObjectFromState(ParseObjectCoder.Instance.Decode(JsonUtilities.Parse(serialization) as IDictionary, Decoder, serviceHub), "_User", serviceHub); + + return CurrentUser = user; + })).Unwrap(), cancellationToken); + } + + public Task ExistsAsync(CancellationToken cancellationToken) => CurrentUser is { } ? Task.FromResult(true) : TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => StorageController.LoadAsync().OnSuccess(t => t.Result.ContainsKey(nameof(CurrentUser)))).Unwrap(), cancellationToken); + + public bool IsCurrent(ParseUser user) + { + lock (Mutex) + return CurrentUser == user; + } + + public void ClearFromMemory() => CurrentUser = default; + + public void ClearFromDisk() + { + lock (Mutex) + { + ClearFromMemory(); + + TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => StorageController.LoadAsync().OnSuccess(t => t.Result.RemoveAsync(nameof(CurrentUser)))).Unwrap().Unwrap(), CancellationToken.None); + } + } + + public Task GetCurrentSessionTokenAsync(IServiceHub serviceHub, CancellationToken cancellationToken = default) => GetAsync(serviceHub, cancellationToken).OnSuccess(task => task.Result?.SessionToken); + + public Task LogOutAsync(IServiceHub serviceHub, CancellationToken cancellationToken = default) => TaskQueue.Enqueue(toAwait => toAwait.ContinueWith(_ => GetAsync(serviceHub, cancellationToken)).Unwrap().OnSuccess(task => ClearFromDisk()), cancellationToken); + } +} diff --git a/Parse/Platform/Users/ParseUser.cs b/Parse/Platform/Users/ParseUser.cs new file mode 100644 index 00000000..365fa2c5 --- /dev/null +++ b/Parse/Platform/Users/ParseUser.cs @@ -0,0 +1,329 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure.Control; +using Parse.Abstractions.Platform.Authentication; +using Parse.Abstractions.Platform.Objects; +using Parse.Infrastructure.Utilities; + +namespace Parse +{ + /// + /// Represents a user for a Parse application. + /// + [ParseClassName("_User")] + public class ParseUser : ParseObject + { + /// + /// Whether the ParseUser has been authenticated on this device. Only an authenticated + /// ParseUser can be saved and deleted. + /// + public bool IsAuthenticated + { + get + { + lock (Mutex) + { + return SessionToken is { } && Services.GetCurrentUser() is { } user && user.ObjectId == ObjectId; + } + } + } + + /// + /// Removes a key from the object's data if it exists. + /// + /// The key to remove. + /// Cannot remove the username key. + public override void Remove(string key) + { + if (key == "username") + { + throw new InvalidOperationException("Cannot remove the username key."); + } + + base.Remove(key); + } + + protected override bool CheckKeyMutable(string key) => !ImmutableKeys.Contains(key); + + internal override void HandleSave(IObjectState serverState) + { + base.HandleSave(serverState); + + SynchronizeAllAuthData(); + CleanupAuthData(); + + MutateState(mutableClone => mutableClone.ServerData.Remove("password")); + } + + public string SessionToken => State.ContainsKey("sessionToken") ? State["sessionToken"] as string : null; + + internal Task SetSessionTokenAsync(string newSessionToken) => SetSessionTokenAsync(newSessionToken, CancellationToken.None); + + internal Task SetSessionTokenAsync(string newSessionToken, CancellationToken cancellationToken) + { + MutateState(mutableClone => mutableClone.ServerData["sessionToken"] = newSessionToken); + return Services.SaveCurrentUserAsync(this); + } + + /// + /// Gets or sets the username. + /// + [ParseFieldName("username")] + public string Username + { + get => GetProperty(null, nameof(Username)); + set => SetProperty(value, nameof(Username)); + } + + /// + /// Sets the password. + /// + [ParseFieldName("password")] + public string Password + { + get => GetProperty(null, nameof(Password)); + set => SetProperty(value, nameof(Password)); + } + + /// + /// Sets the email address. + /// + [ParseFieldName("email")] + public string Email + { + get => GetProperty(null, nameof(Email)); + set => SetProperty(value, nameof(Email)); + } + + internal Task SignUpAsync(Task toAwait, CancellationToken cancellationToken) + { + if (AuthData == null) + { + // TODO (hallucinogen): make an Extension of Task to create Task with exception/canceled. + if (String.IsNullOrEmpty(Username)) + { + TaskCompletionSource tcs = new TaskCompletionSource(); + tcs.TrySetException(new InvalidOperationException("Cannot sign up user with an empty name.")); + return tcs.Task; + } + if (String.IsNullOrEmpty(Password)) + { + TaskCompletionSource tcs = new TaskCompletionSource(); + tcs.TrySetException(new InvalidOperationException("Cannot sign up user with an empty password.")); + return tcs.Task; + } + } + if (!String.IsNullOrEmpty(ObjectId)) + { + TaskCompletionSource tcs = new TaskCompletionSource(); + tcs.TrySetException(new InvalidOperationException("Cannot sign up a user that already exists.")); + return tcs.Task; + } + + IDictionary currentOperations = StartSave(); + + return toAwait.OnSuccess(_ => Services.UserController.SignUpAsync(State, currentOperations, Services, cancellationToken)).Unwrap().ContinueWith(t => + { + if (t.IsFaulted || t.IsCanceled) + { + HandleFailedSave(currentOperations); + } + else + { + HandleSave(t.Result); + } + return t; + }).Unwrap().OnSuccess(_ => Services.SaveCurrentUserAsync(this)).Unwrap(); + } + + /// + /// Signs up a new user. This will create a new ParseUser on the server and will also persist the + /// session on disk so that you can access the user using . A username and + /// password must be set before calling SignUpAsync. + /// + public Task SignUpAsync() => SignUpAsync(CancellationToken.None); + + /// + /// Signs up a new user. This will create a new ParseUser on the server and will also persist the + /// session on disk so that you can access the user using . A username and + /// password must be set before calling SignUpAsync. + /// + /// The cancellation token. + public Task SignUpAsync(CancellationToken cancellationToken) => TaskQueue.Enqueue(toAwait => SignUpAsync(toAwait, cancellationToken), cancellationToken); + + protected override Task SaveAsync(Task toAwait, CancellationToken cancellationToken) + { + lock (Mutex) + { + if (ObjectId is null) + { + throw new InvalidOperationException("You must call SignUpAsync before calling SaveAsync."); + } + + return base.SaveAsync(toAwait, cancellationToken).OnSuccess(_ => Services.CurrentUserController.IsCurrent(this) ? Services.SaveCurrentUserAsync(this) : Task.CompletedTask).Unwrap(); + } + } + + // If this is already the current user, refresh its state on disk. + internal override Task FetchAsyncInternal(Task toAwait, CancellationToken cancellationToken) => base.FetchAsyncInternal(toAwait, cancellationToken).OnSuccess(t => !Services.CurrentUserController.IsCurrent(this) ? Task.FromResult(t.Result) : Services.SaveCurrentUserAsync(this).OnSuccess(_ => t.Result)).Unwrap(); + + internal Task LogOutAsync(Task toAwait, CancellationToken cancellationToken) + { + string oldSessionToken = SessionToken; + if (oldSessionToken == null) + { + return Task.FromResult(0); + } + + // Cleanup in-memory session. + + MutateState(mutableClone => mutableClone.ServerData.Remove("sessionToken")); + Task revokeSessionTask = Services.RevokeSessionAsync(oldSessionToken, cancellationToken); + return Task.WhenAll(revokeSessionTask, Services.CurrentUserController.LogOutAsync(Services, cancellationToken)); + } + + internal Task UpgradeToRevocableSessionAsync() => UpgradeToRevocableSessionAsync(CancellationToken.None); + + internal Task UpgradeToRevocableSessionAsync(CancellationToken cancellationToken) => TaskQueue.Enqueue(toAwait => UpgradeToRevocableSessionAsync(toAwait, cancellationToken), cancellationToken); + + internal Task UpgradeToRevocableSessionAsync(Task toAwait, CancellationToken cancellationToken) + { + string sessionToken = SessionToken; + + return toAwait.OnSuccess(_ => Services.UpgradeToRevocableSessionAsync(sessionToken, cancellationToken)).Unwrap().OnSuccess(task => SetSessionTokenAsync(task.Result)).Unwrap(); + } + + /// + /// Gets the authData for this user. + /// + public IDictionary> AuthData + { + get => TryGetValue("authData", out IDictionary> authData) ? authData : null; + set => this["authData"] = value; + } + + /// + /// Removes null values from authData (which exist temporarily for unlinking) + /// + void CleanupAuthData() + { + lock (Mutex) + { + if (!Services.CurrentUserController.IsCurrent(this)) + { + return; + } + + IDictionary> authData = AuthData; + + if (authData == null) + { + return; + } + + foreach (KeyValuePair> pair in new Dictionary>(authData)) + { + if (pair.Value == null) + { + authData.Remove(pair.Key); + } + } + } + } + +#warning Check if the following properties should be injected via IServiceHub.UserController (except for ImmutableKeys). + + internal static IParseAuthenticationProvider GetProvider(string providerName) => Authenticators.TryGetValue(providerName, out IParseAuthenticationProvider provider) ? provider : null; + + internal static IDictionary Authenticators { get; } = new Dictionary { }; + + internal static HashSet ImmutableKeys { get; } = new HashSet { "sessionToken", "isNew" }; + + /// + /// Synchronizes authData for all providers. + /// + internal void SynchronizeAllAuthData() + { + lock (Mutex) + { + IDictionary> authData = AuthData; + + if (authData == null) + { + return; + } + + foreach (KeyValuePair> pair in authData) + { + SynchronizeAuthData(GetProvider(pair.Key)); + } + } + } + + internal void SynchronizeAuthData(IParseAuthenticationProvider provider) + { + bool restorationSuccess = false; + + lock (Mutex) + { + IDictionary> authData = AuthData; + + if (authData == null || provider == null) + { + return; + } + + if (authData.TryGetValue(provider.AuthType, out IDictionary data)) + { + restorationSuccess = provider.RestoreAuthentication(data); + } + } + + if (!restorationSuccess) + { + UnlinkFromAsync(provider.AuthType, CancellationToken.None); + } + } + + internal Task LinkWithAsync(string authType, IDictionary data, CancellationToken cancellationToken) => TaskQueue.Enqueue(toAwait => + { + IDictionary> authData = AuthData; + + if (authData == null) + { + authData = AuthData = new Dictionary>(); + } + + authData[authType] = data; + AuthData = authData; + + return SaveAsync(cancellationToken); + }, cancellationToken); + + internal Task LinkWithAsync(string authType, CancellationToken cancellationToken) + { + IParseAuthenticationProvider provider = GetProvider(authType); + return provider.AuthenticateAsync(cancellationToken).OnSuccess(t => LinkWithAsync(authType, t.Result, cancellationToken)).Unwrap(); + } + + /// + /// Unlinks a user from a service. + /// + internal Task UnlinkFromAsync(string authType, CancellationToken cancellationToken) => LinkWithAsync(authType, null, cancellationToken); + + /// + /// Checks whether a user is linked to a service. + /// + internal bool IsLinked(string authType) + { + lock (Mutex) + { + return AuthData != null && AuthData.ContainsKey(authType) && AuthData[authType] != null; + } + } + } +} diff --git a/Parse/Platform/Users/ParseUserController.cs b/Parse/Platform/Users/ParseUserController.cs new file mode 100644 index 00000000..635f1470 --- /dev/null +++ b/Parse/Platform/Users/ParseUserController.cs @@ -0,0 +1,48 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure.Control; +using Parse.Abstractions.Infrastructure.Data; +using Parse.Abstractions.Infrastructure.Execution; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Platform.Users; +using Parse.Infrastructure.Utilities; +using Parse.Abstractions.Platform.Objects; +using Parse.Infrastructure.Execution; +using Parse.Infrastructure.Data; + +namespace Parse.Platform.Users +{ + public class ParseUserController : IParseUserController + { + IParseCommandRunner CommandRunner { get; } + + IParseDataDecoder Decoder { get; } + + public bool RevocableSessionEnabled { get; set; } + + public object RevocableSessionEnabledMutex { get; } = new object { }; + + public ParseUserController(IParseCommandRunner commandRunner, IParseDataDecoder decoder) => (CommandRunner, Decoder) = (commandRunner, decoder); + + public Task SignUpAsync(IObjectState state, IDictionary operations, IServiceHub serviceHub, CancellationToken cancellationToken = default) => CommandRunner.RunCommandAsync(new ParseCommand("classes/_User", method: "POST", data: serviceHub.GenerateJSONObjectForSaving(operations)), cancellationToken: cancellationToken).OnSuccess(task => ParseObjectCoder.Instance.Decode(task.Result.Item2, Decoder, serviceHub).MutatedClone(mutableClone => mutableClone.IsNew = true)); + + public Task LogInAsync(string username, string password, IServiceHub serviceHub, CancellationToken cancellationToken = default) => CommandRunner.RunCommandAsync(new ParseCommand($"login?{ParseClient.BuildQueryString(new Dictionary { [nameof(username)] = username, [nameof(password)] = password })}", method: "GET", data: null), cancellationToken: cancellationToken).OnSuccess(task => ParseObjectCoder.Instance.Decode(task.Result.Item2, Decoder, serviceHub).MutatedClone(mutableClone => mutableClone.IsNew = task.Result.Item1 == System.Net.HttpStatusCode.Created)); + + public Task LogInAsync(string authType, IDictionary data, IServiceHub serviceHub, CancellationToken cancellationToken = default) + { + Dictionary authData = new Dictionary + { + [authType] = data + }; + + return CommandRunner.RunCommandAsync(new ParseCommand("users", method: "POST", data: new Dictionary { [nameof(authData)] = authData }), cancellationToken: cancellationToken).OnSuccess(task => ParseObjectCoder.Instance.Decode(task.Result.Item2, Decoder, serviceHub).MutatedClone(mutableClone => mutableClone.IsNew = task.Result.Item1 == System.Net.HttpStatusCode.Created)); + } + + public Task GetUserAsync(string sessionToken, IServiceHub serviceHub, CancellationToken cancellationToken = default) => CommandRunner.RunCommandAsync(new ParseCommand("users/me", method: "GET", sessionToken: sessionToken, data: default), cancellationToken: cancellationToken).OnSuccess(task => ParseObjectCoder.Instance.Decode(task.Result.Item2, Decoder, serviceHub)); + + public Task RequestPasswordResetAsync(string email, CancellationToken cancellationToken = default) => CommandRunner.RunCommandAsync(new ParseCommand("requestPasswordReset", method: "POST", data: new Dictionary { [nameof(email)] = email }), cancellationToken: cancellationToken); + } +} diff --git a/Parse/Public/ParseClient.cs b/Parse/Public/ParseClient.cs deleted file mode 100644 index 5cbca097..00000000 --- a/Parse/Public/ParseClient.cs +++ /dev/null @@ -1,296 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using Parse.Common.Internal; -using Parse.Internal.Utilities; - -namespace Parse -{ - /// - /// ParseClient contains static functions that handle global - /// configuration for the Parse library. - /// - public static partial class ParseClient - { - internal static readonly string[] DateFormatStrings = - { - // Official ISO format - "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'", - - // It's possible that the string converter server-side may trim trailing zeroes, - // so these two formats cover ourselves from that. - "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'ff'Z'", - "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'f'Z'", - - }; - - /// - /// Represents the configuration of the Parse SDK. - /// - public struct Configuration - { - /// - /// A unit that can generate a relative path to a persistent storage file. - /// - public interface IStorageConfiguration - { - /// - /// The corresponding relative path generated by this . - /// - string RelativeStorageFilePath { get; } - } - - /// - /// A configuration of the Parse SDK persistent storage location based on product metadata such as company name and product name. - /// - public struct MetadataBasedStorageConfiguration : IStorageConfiguration - { - /// - /// An instance of with inferred values based on the entry assembly. Should be used with . - /// - /// Should not be used with Unity. - public static MetadataBasedStorageConfiguration NoCompanyInferred { get; } = new MetadataBasedStorageConfiguration { CompanyName = Assembly.GetEntryAssembly().GetName().Name, ProductName = String.Empty }; - - /// - /// The name of the company that owns the product specified by . - /// - public string CompanyName { get; set; } - - /// - /// The name of the product that is using the Parse .NET SDK. - /// - public string ProductName { get; set; } - - /// - /// The corresponding relative path generated by this . - /// - public string RelativeStorageFilePath => Path.Combine(CompanyName ?? "Parse", ProductName ?? "_global", $"{CurrentConfiguration.VersionInfo.DisplayVersion ?? "1.0.0.0"}.cachefile"); - } - - /// - /// A configuration of the Parse SDK persistent storage location based on an identifier. - /// - public struct IdentifierBasedStorageConfiguration : IStorageConfiguration - { - internal static IdentifierBasedStorageConfiguration Fallback { get; } = new IdentifierBasedStorageConfiguration { IsFallback = true }; - - /// - /// Dictates whether or not this instance should act as a fallback for when has not yet been initialized but the storage path is needed. - /// - internal bool IsFallback { get; set; } - - /// - /// The identifier that all Parse SDK cache files should be labelled with. - /// - public string Identifier { get; set; } - - /// - /// The corresponding relative path generated by this . - /// - /// This will cause a .cachefile file extension to be added to the cache file in order to prevent the creation of files with unwanted extensions due to the value of containing periods. - public string RelativeStorageFilePath - { - get - { - FileInfo file = default; - while ((file = StorageManager.GetWrapperForRelativePersistentStorageFilePath(GeneratePath())).Exists && IsFallback); - - return file.FullName; - } - } - - /// - /// Generates a path for use in the getter. - /// - /// A potential path to the cachefile - string GeneratePath() => Path.Combine("Parse", IsFallback ? "_fallback" : "_global", $"{(IsFallback ? new Random { }.Next().ToString() : Identifier)}.cachefile"); - } - - /// - /// In the event that you would like to use the Parse SDK - /// from a completely portable project, with no platform-specific library required, - /// to get full access to all of our features available on Parse Dashboard - /// (A/B testing, slow queries, etc.), you must set the values of this struct - /// to be appropriate for your platform. - /// - /// Any values set here will overwrite those that are automatically configured by - /// any platform-specific migration library your app includes. - /// - public struct VersionInformation - { - /// - /// An instance of with inferred values based on the entry assembly. - /// - /// Should not be used with Unity. - public static VersionInformation Inferred { get; } = new VersionInformation { BuildVersion = Assembly.GetEntryAssembly().GetName().Version.Build.ToString(), DisplayVersion = Assembly.GetEntryAssembly().GetName().Version.ToString(), OSVersion = Environment.OSVersion.ToString() }; - - /// - /// The build number of your app. - /// - public string BuildVersion { get; set; } - - /// - /// The human friendly version number of your app. - /// - public string DisplayVersion { get; set; } - - /// - /// The operating system version of the platform the SDK is operating in.. - /// - public string OSVersion { get; set; } - - /// - /// Gets a value for whether or not this instance of is populated with default values. - /// - internal bool IsDefault => BuildVersion is null && DisplayVersion is null && OSVersion is null; - - /// - /// Gets a value for whether or not this instance of can currently be used for the generation of . - /// - internal bool CanBeUsedForInference => !(IsDefault || String.IsNullOrWhiteSpace(DisplayVersion)); - } - - /// - /// The App ID of your app. - /// - public string ApplicationID { get; set; } - - /// - /// A URI pointing to the target Parse Server instance hosting the app targeted by . - /// - public string ServerURI { get; set; } - - /// - /// The .NET Key for the Parse app targeted by . - /// - public string Key { get; set; } - - /// - /// The Master Key for the Parse app targeted by . - /// - public string MasterKey - { - get => AuxiliaryHeaders?["X-Parse-Master-Key"]; - set => (AuxiliaryHeaders ?? (AuxiliaryHeaders = new Dictionary { }))["X-Parse-Master-Key"] = value; - } - - /// - /// Additional HTTP headers to be sent with network requests from the SDK. - /// - public IDictionary AuxiliaryHeaders { get; set; } - - /// - /// The version information of your application environment. - /// - public VersionInformation VersionInfo { get; set; } - - /// - /// The that Parse should use when generating cache files. - /// - public IStorageConfiguration StorageConfiguration { get; set; } - } - - private static readonly object mutex = new object(); - - - // TODO: Investigate if the version string header can be changed to simple "net-". - static ParseClient() => VersionString = "net-portable-" + Version; //ParseModuleController.Instance.ScanForModules(); - - /// - /// The current configuration that parse has been initialized with. - /// - public static Configuration CurrentConfiguration { get; internal set; } - internal static string MasterKey { get; set; } - - internal static Version Version => new AssemblyName(typeof(ParseClient).GetTypeInfo().Assembly.FullName).Version; - internal static string VersionString { get; } - - /// - /// Authenticates this client as belonging to your application. This must be - /// called before your application can use the Parse library. The recommended - /// way is to put a call to ParseClient.Initialize in your - /// Application startup. - /// - /// The Application ID provided in the Parse dashboard. - /// - /// The server URI provided in the Parse dashboard. - /// - public static void Initialize(string identifier, string serverURI) => Initialize(new Configuration { ApplicationID = identifier, ServerURI = serverURI }); - - /// - /// Authenticates this client as belonging to your application. This must be - /// called before your application can use the Parse library. The recommended - /// way is to put a call to ParseClient.Initialize in your - /// Application startup. - /// - /// The configuration to initialize Parse with. - /// - public static void Initialize(Configuration configuration) - { - lock (mutex) - { - configuration.ServerURI = configuration.ServerURI ?? "https://api.parse.com/1/"; - //if (configuration.Server == null || configuration.Server.Length < 11) throw new ArgumentNullException("Since the official parse server has shut down, you must specify the URI that points to another implementation."); - - switch (configuration.VersionInfo) - { - case Configuration.VersionInformation info when info.CanBeUsedForInference: - break; - case Configuration.VersionInformation info when !info.IsDefault: - configuration.VersionInfo = new Configuration.VersionInformation { BuildVersion = info.BuildVersion, OSVersion = info.OSVersion, DisplayVersion = Configuration.VersionInformation.Inferred.DisplayVersion }; - break; - default: - configuration.VersionInfo = Configuration.VersionInformation.Inferred; - break; - } - - switch (configuration.StorageConfiguration) - { - case null: - configuration.StorageConfiguration = Configuration.MetadataBasedStorageConfiguration.NoCompanyInferred; - break; - default: - break; - } - - CurrentConfiguration = configuration; - - ParseObject.RegisterSubclass(); - ParseObject.RegisterSubclass(); - ParseObject.RegisterSubclass(); - ParseObject.RegisterSubclass(); - - ParseModuleController.Instance.ParseDidInitialize(); - } - } - - /// - /// Reflects a change in the Parse SDK storage configuration by copying all of the cached data into the new location. - /// - /// - /// - public static async Task ReflectStorageChangeAsync(string originalRelativePath) => await StorageManager.TransferAsync(StorageManager.GetWrapperForRelativePersistentStorageFilePath(originalRelativePath).FullName, StorageManager.PersistentStorageFilePath); - - internal static string BuildQueryString(IDictionary parameters) => String.Join("&", (from pair in parameters let valueString = pair.Value as string select $"{Uri.EscapeDataString(pair.Key)}={Uri.EscapeDataString(String.IsNullOrEmpty(valueString) ? Json.Encode(pair.Value) : valueString)}").ToArray()); - - internal static IDictionary DecodeQueryString(string queryString) - { - Dictionary dict = new Dictionary(); - foreach (string pair in queryString.Split('&')) - { - string[] parts = pair.Split(new char[] { '=' }, 2); - dict[parts[0]] = parts.Length == 2 ? Uri.UnescapeDataString(parts[1].Replace("+", " ")) : null; - } - return dict; - } - - internal static IDictionary DeserializeJsonString(string jsonData) => Json.Parse(jsonData) as IDictionary; - - internal static string SerializeJsonString(IDictionary jsonData) => Json.Encode(jsonData); - } -} diff --git a/Parse/Public/ParseConfig.cs b/Parse/Public/ParseConfig.cs deleted file mode 100644 index 7756074c..00000000 --- a/Parse/Public/ParseConfig.cs +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using Parse.Core.Internal; -using Parse.Utilities; -using Parse.Common.Internal; - -namespace Parse -{ - /// - /// The ParseConfig is a representation of the remote configuration object, - /// that enables you to add things like feature gating, a/b testing or simple "Message of the day". - /// - public class ParseConfig : IJsonConvertible - { - private IDictionary properties = new Dictionary(); - - /// - /// Gets the latest fetched ParseConfig. - /// - /// ParseConfig object - public static ParseConfig CurrentConfig - { - get - { - Task task = ConfigController.CurrentConfigController.GetCurrentConfigAsync(); - task.Wait(); - return task.Result; - } - } - - internal static void ClearCurrentConfig() - { - ConfigController.CurrentConfigController.ClearCurrentConfigAsync().Wait(); - } - - internal static void ClearCurrentConfigInMemory() - { - ConfigController.CurrentConfigController.ClearCurrentConfigInMemoryAsync().Wait(); - } - - private static IParseConfigController ConfigController - { - get { return ParseCorePlugins.Instance.ConfigController; } - } - - internal ParseConfig() - : base() - { - } - - internal ParseConfig(IDictionary fetchedConfig) - { - var props = ParseDecoder.Instance.Decode(fetchedConfig["params"]) as IDictionary; - properties = props; - } - - /// - /// Retrieves the ParseConfig asynchronously from the server. - /// - /// ParseConfig object that was fetched - public static Task GetAsync() - { - return GetAsync(CancellationToken.None); - } - - /// - /// Retrieves the ParseConfig asynchronously from the server. - /// - /// The cancellation token. - /// ParseConfig object that was fetched - public static Task GetAsync(CancellationToken cancellationToken) - { - return ConfigController.FetchConfigAsync(ParseUser.CurrentSessionToken, cancellationToken); - } - - /// - /// Gets a value for the key of a particular type. - /// - /// The type to convert the value to. Supported types are - /// ParseObject and its descendents, Parse types such as ParseRelation and ParseGeopoint, - /// primitive types,IList<T>, IDictionary<string, T> and strings. - /// The key of the element to get. - /// The property is retrieved - /// and is not found. - /// The property under this - /// key was found, but of a different type. - public T Get(string key) - { - return Conversion.To(this.properties[key]); - } - - /// - /// Populates result with the value for the key, if possible. - /// - /// The desired type for the value. - /// The key to retrieve a value for. - /// The value for the given key, converted to the - /// requested type, or null if unsuccessful. - /// true if the lookup and conversion succeeded, otherwise false. - public bool TryGetValue(string key, out T result) - { - if (this.properties.ContainsKey(key)) - { - try - { - var temp = Conversion.To(this.properties[key]); - result = temp; - return true; - } - catch - { - // Could not convert, do nothing - } - } - result = default(T); - return false; - } - - /// - /// Gets a value on the config. - /// - /// The key for the parameter. - /// The property is - /// retrieved and is not found. - /// The value for the key. - virtual public object this[string key] - { - get - { - return this.properties[key]; - } - } - - IDictionary IJsonConvertible.ToJSON() - { - return new Dictionary { - { "params", NoObjectsEncoder.Instance.Encode(properties) } - }; - } - } -} diff --git a/Parse/Public/ParseDownloadProgressEventArgs.cs b/Parse/Public/ParseDownloadProgressEventArgs.cs deleted file mode 100644 index b9da12ec..00000000 --- a/Parse/Public/ParseDownloadProgressEventArgs.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; - -namespace Parse -{ - /// - /// Represents download progress. - /// - public class ParseDownloadProgressEventArgs : EventArgs - { - public ParseDownloadProgressEventArgs() { } - - /// - /// Gets the progress (a number between 0.0 and 1.0) of a download. - /// - public double Progress { get; set; } - } -} diff --git a/Parse/Public/ParseExtensions.cs b/Parse/Public/ParseExtensions.cs deleted file mode 100644 index 5dae9417..00000000 --- a/Parse/Public/ParseExtensions.cs +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using System.Linq; -using Parse.Core.Internal; -using Parse.Common.Internal; - -namespace Parse -{ - /// - /// Provides convenience extension methods for working with collections - /// of ParseObjects so that you can easily save and fetch them in batches. - /// - public static class ParseExtensions - { - /// - /// Saves all of the ParseObjects in the enumeration. Equivalent to - /// calling . - /// - /// The objects to save. - public static Task SaveAllAsync(this IEnumerable objects) where T : ParseObject - { - return ParseObject.SaveAllAsync(objects); - } - - /// - /// Saves all of the ParseObjects in the enumeration. Equivalent to - /// calling - /// . - /// - /// The objects to save. - /// The cancellation token. - public static Task SaveAllAsync( - this IEnumerable objects, CancellationToken cancellationToken) where T : ParseObject - { - return ParseObject.SaveAllAsync(objects, cancellationToken); - } - - /// - /// Fetches all of the objects in the enumeration. Equivalent to - /// calling . - /// - /// The objects to save. - public static Task> FetchAllAsync(this IEnumerable objects) - where T : ParseObject - { - return ParseObject.FetchAllAsync(objects); - } - - /// - /// Fetches all of the objects in the enumeration. Equivalent to - /// calling - /// . - /// - /// The objects to fetch. - /// The cancellation token. - public static Task> FetchAllAsync( - this IEnumerable objects, CancellationToken cancellationToken) - where T : ParseObject - { - return ParseObject.FetchAllAsync(objects, cancellationToken); - } - - /// - /// Fetches all of the objects in the enumeration that don't already have - /// data. Equivalent to calling - /// . - /// - /// The objects to fetch. - public static Task> FetchAllIfNeededAsync( - this IEnumerable objects) - where T : ParseObject - { - return ParseObject.FetchAllIfNeededAsync(objects); - } - - /// - /// Fetches all of the objects in the enumeration that don't already have - /// data. Equivalent to calling - /// . - /// - /// The objects to fetch. - /// The cancellation token. - public static Task> FetchAllIfNeededAsync( - this IEnumerable objects, CancellationToken cancellationToken) - where T : ParseObject - { - return ParseObject.FetchAllIfNeededAsync(objects, cancellationToken); - } - - /// - /// Constructs a query that is the or of the given queries. - /// - /// The type of ParseObject being queried. - /// An initial query to 'or' with additional queries. - /// The list of ParseQueries to 'or' together. - /// A query that is the or of the given queries. - public static ParseQuery Or(this ParseQuery source, params ParseQuery[] queries) - where T : ParseObject - { - return ParseQuery.Or(queries.Concat(new[] { source })); - } - - /// - /// Fetches this object with the data from the server. - /// - public static Task FetchAsync(this T obj) where T : ParseObject - { - return obj.FetchAsyncInternal(CancellationToken.None).OnSuccess(t => (T) t.Result); - } - - /// - /// Fetches this object with the data from the server. - /// - /// The ParseObject to fetch. - /// The cancellation token. - public static Task FetchAsync(this T obj, CancellationToken cancellationToken) - where T : ParseObject - { - return obj.FetchAsyncInternal(cancellationToken).OnSuccess(t => (T) t.Result); - } - - /// - /// If this ParseObject has not been fetched (i.e. returns - /// false), fetches this object with the data from the server. - /// - /// The ParseObject to fetch. - public static Task FetchIfNeededAsync(this T obj) where T : ParseObject - { - return obj.FetchIfNeededAsyncInternal(CancellationToken.None).OnSuccess(t => (T) t.Result); - } - - /// - /// If this ParseObject has not been fetched (i.e. returns - /// false), fetches this object with the data from the server. - /// - /// The ParseObject to fetch. - /// The cancellation token. - public static Task FetchIfNeededAsync(this T obj, CancellationToken cancellationToken) - where T : ParseObject - { - return obj.FetchIfNeededAsyncInternal(cancellationToken).OnSuccess(t => (T) t.Result); - } - } -} diff --git a/Parse/Public/ParseInstallation.cs b/Parse/Public/ParseInstallation.cs deleted file mode 100644 index 4622e7c2..00000000 --- a/Parse/Public/ParseInstallation.cs +++ /dev/null @@ -1,431 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using Parse; -using Parse.Common.Internal; -using Parse.Core.Internal; -using Parse.Push.Internal; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Parse -{ - - /// - /// Represents this app installed on this device. Use this class to track information you want - /// to sample from (i.e. if you update a field on app launch, you can issue a query to see - /// the number of devices which were active in the last N hours). - /// - [ParseClassName("_Installation")] - public partial class ParseInstallation : ParseObject - { - private static readonly HashSet readOnlyKeys = new HashSet { - "deviceType", "deviceUris", "installationId", "timeZone", "localeIdentifier", "parseVersion", "appName", "appIdentifier", "appVersion", "pushType" - }; - - internal static IParseCurrentInstallationController CurrentInstallationController - { - get - { - return ParsePushPlugins.Instance.CurrentInstallationController; - } - } - - internal static IDeviceInfoController DeviceInfoController - { - get - { - return ParsePushPlugins.Instance.DeviceInfoController; - } - } - - /// - /// Constructs a new ParseInstallation. Generally, you should not need to construct - /// ParseInstallations yourself. Instead use . - /// - public ParseInstallation() - : base() - { - } - - /// - /// Gets the ParseInstallation representing this app on this device. - /// - public static ParseInstallation CurrentInstallation - { - get - { - var task = CurrentInstallationController.GetAsync(CancellationToken.None); - // TODO (hallucinogen): this will absolutely break on Unity, but how should we resolve this? - task.Wait(); - return task.Result; - } - } - - internal static void ClearInMemoryInstallation() - { - CurrentInstallationController.ClearFromMemory(); - } - - /// - /// Constructs a for ParseInstallations. - /// - /// - /// Only the following types of queries are allowed for installations: - /// - /// - /// query.GetAsync(objectId) - /// query.WhereEqualTo(key, value) - /// query.WhereMatchesKeyInQuery<TOther>(key, keyInQuery, otherQuery) - /// - /// - /// You can add additional query conditions, but one of the above must appear as a top-level AND - /// clause in the query. - /// - public static ParseQuery Query - { - get - { - return new ParseQuery(); - } - } - - /// - /// A GUID that uniquely names this app installed on this device. - /// - [ParseFieldName("installationId")] - public Guid InstallationId - { - get - { - string installationIdString = GetProperty("InstallationId"); - Guid? installationId = null; - try - { - installationId = new Guid(installationIdString); - } - catch (Exception) - { - // Do nothing. - } - - return installationId.Value; - } - internal set - { - Guid installationId = value; - SetProperty(installationId.ToString(), "InstallationId"); - } - } - - /// - /// The runtime target of this installation object. - /// - [ParseFieldName("deviceType")] - public string DeviceType - { - get { return GetProperty("DeviceType"); } - internal set { SetProperty(value, "DeviceType"); } - } - - /// - /// The user-friendly display name of this application. - /// - [ParseFieldName("appName")] - public string AppName - { - get { return GetProperty("AppName"); } - internal set { SetProperty(value, "AppName"); } - } - - /// - /// A version string consisting of Major.Minor.Build.Revision. - /// - [ParseFieldName("appVersion")] - public string AppVersion - { - get { return GetProperty("AppVersion"); } - internal set { SetProperty(value, "AppVersion"); } - } - - /// - /// The system-dependent unique identifier of this installation. This identifier should be - /// sufficient to distinctly name an app on stores which may allow multiple apps with the - /// same display name. - /// - [ParseFieldName("appIdentifier")] - public string AppIdentifier - { - get { return GetProperty("AppIdentifier"); } - internal set { SetProperty(value, "AppIdentifier"); } - } - - /// - /// The time zone in which this device resides. This string is in the tz database format - /// Parse uses for local-time pushes. Due to platform restrictions, the mapping is less - /// granular on Windows than it may be on other systems. E.g. The zones - /// America/Vancouver America/Dawson America/Whitehorse, America/Tijuana, PST8PDT, and - /// America/Los_Angeles are all reported as America/Los_Angeles. - /// - [ParseFieldName("timeZone")] - public string TimeZone - { - get { return GetProperty("TimeZone"); } - private set { SetProperty(value, "TimeZone"); } - } - - /// - /// The users locale. This field gets automatically populated by the SDK. - /// Can be null (Parse Push uses default language in this case). - /// - [ParseFieldName("localeIdentifier")] - public string LocaleIdentifier - { - get { return GetProperty("LocaleIdentifier"); } - private set { SetProperty(value, "LocaleIdentifier"); } - } - - /// - /// Gets the locale identifier in the format: [language code]-[COUNTRY CODE]. - /// - /// The locale identifier in the format: [language code]-[COUNTRY CODE]. - private string GetLocaleIdentifier() - { - string languageCode = null; - string countryCode = null; - if (CultureInfo.CurrentCulture != null) - { - languageCode = CultureInfo.CurrentCulture.TwoLetterISOLanguageName; - } - if (RegionInfo.CurrentRegion != null) - { - countryCode = RegionInfo.CurrentRegion.TwoLetterISORegionName; - } - if (string.IsNullOrEmpty(countryCode)) - { - return languageCode; - } - else - { - return string.Format("{0}-{1}", languageCode, countryCode); - } - } - - /// - /// The version of the Parse SDK used to build this application. - /// - [ParseFieldName("parseVersion")] - public Version ParseVersion - { - get - { - var versionString = GetProperty("ParseVersion"); - Version version = null; - try - { - version = new Version(versionString); - } - catch (Exception) - { - // Do nothing. - } - - return version; - } - private set - { - Version version = value; - SetProperty(version.ToString(), "ParseVersion"); - } - } - - /// - /// A sequence of arbitrary strings which are used to identify this installation for push notifications. - /// By convention, the empty string is known as the "Broadcast" channel. - /// - [ParseFieldName("channels")] - public IList Channels - { - get { return GetProperty>("Channels"); } - set { SetProperty(value, "Channels"); } - } - - protected override bool IsKeyMutable(string key) - { - return !readOnlyKeys.Contains(key); - } - - protected override Task SaveAsync(Task toAwait, CancellationToken cancellationToken) - { - Task platformHookTask = null; - if (CurrentInstallationController.IsCurrent(this)) - { - var configuration = ParseClient.CurrentConfiguration; - - // 'this' is required in order for the extension method to be used. - this.SetIfDifferent("deviceType", DeviceInfoController.DeviceType); - this.SetIfDifferent("timeZone", DeviceInfoController.DeviceTimeZone); - this.SetIfDifferent("localeIdentifier", GetLocaleIdentifier()); - this.SetIfDifferent("parseVersion", GetParseVersion().ToString()); - this.SetIfDifferent("appVersion", configuration.VersionInfo.BuildVersion ?? DeviceInfoController.AppBuildVersion); - this.SetIfDifferent("appIdentifier", DeviceInfoController.AppIdentifier); - this.SetIfDifferent("appName", DeviceInfoController.AppName); - - platformHookTask = DeviceInfoController.ExecuteParseInstallationSaveHookAsync(this); - } - - return platformHookTask.Safe().OnSuccess(_ => - { - return base.SaveAsync(toAwait, cancellationToken); - }).Unwrap().OnSuccess(_ => - { - if (CurrentInstallationController.IsCurrent(this)) - { - return Task.FromResult(0); - } - return CurrentInstallationController.SetAsync(this, cancellationToken); - }).Unwrap(); - } - - private Version GetParseVersion() - { - return new AssemblyName(typeof(ParseInstallation).GetTypeInfo().Assembly.FullName).Version; - } - - /// - /// This mapping of Windows names to a standard everyone else uses is maintained - /// by the Unicode consortium, which makes this officially the first helpful - /// interaction between Unicode and Microsoft. - /// Unfortunately this is a little lossy in that we only store the first mapping in each zone because - /// Microsoft does not give us more granular location information. - /// Built from http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/zone_tzid.html - /// - internal static readonly Dictionary TimeZoneNameMap = new Dictionary() { - {"Dateline Standard Time", "Etc/GMT+12"}, - {"UTC-11", "Etc/GMT+11"}, - {"Hawaiian Standard Time", "Pacific/Honolulu"}, - {"Alaskan Standard Time", "America/Anchorage"}, - {"Pacific Standard Time (Mexico)", "America/Santa_Isabel"}, - {"Pacific Standard Time", "America/Los_Angeles"}, - {"US Mountain Standard Time", "America/Phoenix"}, - {"Mountain Standard Time (Mexico)", "America/Chihuahua"}, - {"Mountain Standard Time", "America/Denver"}, - {"Central America Standard Time", "America/Guatemala"}, - {"Central Standard Time", "America/Chicago"}, - {"Central Standard Time (Mexico)", "America/Mexico_City"}, - {"Canada Central Standard Time", "America/Regina"}, - {"SA Pacific Standard Time", "America/Bogota"}, - {"Eastern Standard Time", "America/New_York"}, - {"US Eastern Standard Time", "America/Indianapolis"}, - {"Venezuela Standard Time", "America/Caracas"}, - {"Paraguay Standard Time", "America/Asuncion"}, - {"Atlantic Standard Time", "America/Halifax"}, - {"Central Brazilian Standard Time", "America/Cuiaba"}, - {"SA Western Standard Time", "America/La_Paz"}, - {"Pacific SA Standard Time", "America/Santiago"}, - {"Newfoundland Standard Time", "America/St_Johns"}, - {"E. South America Standard Time", "America/Sao_Paulo"}, - {"Argentina Standard Time", "America/Buenos_Aires"}, - {"SA Eastern Standard Time", "America/Cayenne"}, - {"Greenland Standard Time", "America/Godthab"}, - {"Montevideo Standard Time", "America/Montevideo"}, - {"Bahia Standard Time", "America/Bahia"}, - {"UTC-02", "Etc/GMT+2"}, - {"Azores Standard Time", "Atlantic/Azores"}, - {"Cape Verde Standard Time", "Atlantic/Cape_Verde"}, - {"Morocco Standard Time", "Africa/Casablanca"}, - {"UTC", "Etc/GMT"}, - {"GMT Standard Time", "Europe/London"}, - {"Greenwich Standard Time", "Atlantic/Reykjavik"}, - {"W. Europe Standard Time", "Europe/Berlin"}, - {"Central Europe Standard Time", "Europe/Budapest"}, - {"Romance Standard Time", "Europe/Paris"}, - {"Central European Standard Time", "Europe/Warsaw"}, - {"W. Central Africa Standard Time", "Africa/Lagos"}, - {"Namibia Standard Time", "Africa/Windhoek"}, - {"GTB Standard Time", "Europe/Bucharest"}, - {"Middle East Standard Time", "Asia/Beirut"}, - {"Egypt Standard Time", "Africa/Cairo"}, - {"Syria Standard Time", "Asia/Damascus"}, - {"E. Europe Standard Time", "Asia/Nicosia"}, - {"South Africa Standard Time", "Africa/Johannesburg"}, - {"FLE Standard Time", "Europe/Kiev"}, - {"Turkey Standard Time", "Europe/Istanbul"}, - {"Israel Standard Time", "Asia/Jerusalem"}, - {"Jordan Standard Time", "Asia/Amman"}, - {"Arabic Standard Time", "Asia/Baghdad"}, - {"Kaliningrad Standard Time", "Europe/Kaliningrad"}, - {"Arab Standard Time", "Asia/Riyadh"}, - {"E. Africa Standard Time", "Africa/Nairobi"}, - {"Iran Standard Time", "Asia/Tehran"}, - {"Arabian Standard Time", "Asia/Dubai"}, - {"Azerbaijan Standard Time", "Asia/Baku"}, - {"Russian Standard Time", "Europe/Moscow"}, - {"Mauritius Standard Time", "Indian/Mauritius"}, - {"Georgian Standard Time", "Asia/Tbilisi"}, - {"Caucasus Standard Time", "Asia/Yerevan"}, - {"Afghanistan Standard Time", "Asia/Kabul"}, - {"Pakistan Standard Time", "Asia/Karachi"}, - {"West Asia Standard Time", "Asia/Tashkent"}, - {"India Standard Time", "Asia/Calcutta"}, - {"Sri Lanka Standard Time", "Asia/Colombo"}, - {"Nepal Standard Time", "Asia/Katmandu"}, - {"Central Asia Standard Time", "Asia/Almaty"}, - {"Bangladesh Standard Time", "Asia/Dhaka"}, - {"Ekaterinburg Standard Time", "Asia/Yekaterinburg"}, - {"Myanmar Standard Time", "Asia/Rangoon"}, - {"SE Asia Standard Time", "Asia/Bangkok"}, - {"N. Central Asia Standard Time", "Asia/Novosibirsk"}, - {"China Standard Time", "Asia/Shanghai"}, - {"North Asia Standard Time", "Asia/Krasnoyarsk"}, - {"Singapore Standard Time", "Asia/Singapore"}, - {"W. Australia Standard Time", "Australia/Perth"}, - {"Taipei Standard Time", "Asia/Taipei"}, - {"Ulaanbaatar Standard Time", "Asia/Ulaanbaatar"}, - {"North Asia East Standard Time", "Asia/Irkutsk"}, - {"Tokyo Standard Time", "Asia/Tokyo"}, - {"Korea Standard Time", "Asia/Seoul"}, - {"Cen. Australia Standard Time", "Australia/Adelaide"}, - {"AUS Central Standard Time", "Australia/Darwin"}, - {"E. Australia Standard Time", "Australia/Brisbane"}, - {"AUS Eastern Standard Time", "Australia/Sydney"}, - {"West Pacific Standard Time", "Pacific/Port_Moresby"}, - {"Tasmania Standard Time", "Australia/Hobart"}, - {"Yakutsk Standard Time", "Asia/Yakutsk"}, - {"Central Pacific Standard Time", "Pacific/Guadalcanal"}, - {"Vladivostok Standard Time", "Asia/Vladivostok"}, - {"New Zealand Standard Time", "Pacific/Auckland"}, - {"UTC+12", "Etc/GMT-12"}, - {"Fiji Standard Time", "Pacific/Fiji"}, - {"Magadan Standard Time", "Asia/Magadan"}, - {"Tonga Standard Time", "Pacific/Tongatapu"}, - {"Samoa Standard Time", "Pacific/Apia"} - }; - - /// - /// This is a mapping of odd TimeZone offsets to their respective IANA codes across the world. - /// This list was compiled from painstakingly pouring over the information available at - /// https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. - /// - internal static readonly Dictionary TimeZoneOffsetMap = new Dictionary() { - { new TimeSpan(12, 45, 0), "Pacific/Chatham" }, - { new TimeSpan(10, 30, 0), "Australia/Lord_Howe" }, - { new TimeSpan(9, 30, 0), "Australia/Adelaide" }, - { new TimeSpan(8, 45, 0), "Australia/Eucla" }, - { new TimeSpan(8, 30, 0), "Asia/Pyongyang" }, // Parse in North Korea confirmed. - { new TimeSpan(6, 30, 0), "Asia/Rangoon" }, - { new TimeSpan(5, 45, 0), "Asia/Kathmandu" }, - { new TimeSpan(5, 30, 0), "Asia/Colombo" }, - { new TimeSpan(4, 30, 0), "Asia/Kabul" }, - { new TimeSpan(3, 30, 0), "Asia/Tehran" }, - { new TimeSpan(-3, 30, 0), "America/St_Johns" }, - { new TimeSpan(-4, 30, 0), "America/Caracas" }, - { new TimeSpan(-9, 30, 0), "Pacific/Marquesas" }, - }; - } -} diff --git a/Parse/Public/ParseObject.cs b/Parse/Public/ParseObject.cs deleted file mode 100644 index 0c7fde6a..00000000 --- a/Parse/Public/ParseObject.cs +++ /dev/null @@ -1,1697 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using Parse.Core.Internal; -using Parse.Utilities; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Net; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using Parse.Common.Internal; - -namespace Parse -{ - /// - /// The ParseObject is a local representation of data that can be saved and - /// retrieved from the Parse cloud. - /// - /// - /// The basic workflow for creating new data is to construct a new ParseObject, - /// use the indexer to fill it with data, and then use SaveAsync() to persist to the - /// database. - /// - /// - /// The basic workflow for accessing existing data is to use a ParseQuery - /// to specify which existing data to retrieve. - /// - /// - public class ParseObject : IEnumerable>, INotifyPropertyChanged - { - private static readonly string AutoClassName = "_Automatic"; - - private static readonly bool isCompiledByIL2CPP = false; - - internal readonly object mutex = new object(); - - private readonly LinkedList> operationSetQueue = new LinkedList>(); - private readonly IDictionary estimatedData = new Dictionary(); - - private static readonly ThreadLocal isCreatingPointer = new ThreadLocal(() => false); - - private bool hasBeenFetched; - private bool dirty; - internal TaskQueue taskQueue = new TaskQueue(); - - private IObjectState state; - internal void MutateState(Action func) - { - lock (mutex) - { - state = state.MutatedClone(func); - - // Refresh the estimated data. - RebuildEstimatedData(); - } - } - - internal IObjectState State => state; - - internal static IParseObjectController ObjectController => ParseCorePlugins.Instance.ObjectController; - - internal static IObjectSubclassingController SubclassingController => ParseCorePlugins.Instance.SubclassingController; - - #region ParseObject Creation - - /// - /// Constructor for use in ParseObject subclasses. Subclasses must specify a ParseClassName attribute. - /// - protected ParseObject() : this(AutoClassName) { } - - /// - /// Constructs a new ParseObject with no data in it. A ParseObject constructed in this way will - /// not have an ObjectId and will not persist to the database until - /// is called. - /// - /// - /// Class names must be alphanumerical plus underscore, and start with a letter. It is recommended - /// to name classes in CamelCaseLikeThis. - /// - /// The className for this ParseObject. - public ParseObject(string className) - { - // We use a ThreadLocal rather than passing a parameter so that createWithoutData can do the - // right thing with subclasses. It's ugly and terrible, but it does provide the development - // experience we generally want, so... yeah. Sorry to whomever has to deal with this in the - // future. I pinky-swear we won't make a habit of this -- you believe me, don't you? - var isPointer = isCreatingPointer.Value; - isCreatingPointer.Value = false; - - if (className == null) - throw new ArgumentException("You must specify a Parse class name when creating a new ParseObject."); - if (AutoClassName.Equals(className)) - className = SubclassingController.GetClassName(GetType()); - // If this is supposed to be created by a factory but wasn't, throw an exception - if (!SubclassingController.IsTypeValid(className, GetType())) - throw new ArgumentException("You must create this type of ParseObject using ParseObject.Create() or the proper subclass."); - state = new MutableObjectState { ClassName = className }; - OnPropertyChanged("ClassName"); - - operationSetQueue.AddLast(new Dictionary()); - if (!isPointer) - { - hasBeenFetched = true; - IsDirty = true; - SetDefaultValues(); - } - else - { - IsDirty = false; - hasBeenFetched = false; - } - } - - /// - /// Creates a new ParseObject based upon a class name. If the class name is a special type (e.g. - /// for ), then the appropriate type of ParseObject is returned. - /// - /// The class of object to create. - /// A new ParseObject for the given class name. - public static ParseObject Create(string className) => SubclassingController.Instantiate(className); - - /// - /// Creates a reference to an existing ParseObject for use in creating associations between - /// ParseObjects. Calling on this object will return - /// false until has been called. - /// No network request will be made. - /// - /// The object's class. - /// The object id for the referenced object. - /// A ParseObject without data. - public static ParseObject CreateWithoutData(string className, string objectId) - { - isCreatingPointer.Value = true; - try - { - var result = SubclassingController.Instantiate(className); - result.ObjectId = objectId; - result.IsDirty = false; // Left in because the property setter might be doing something funky. - return result.IsDirty ? throw new InvalidOperationException("A ParseObject subclass default constructor must not make changes to the object that cause it to be dirty.") : result; - } - finally { isCreatingPointer.Value = false; } - } - - /// - /// Creates a new ParseObject based upon a given subclass type. - /// - /// A new ParseObject for the given class name. - public static T Create() where T : ParseObject => (T) SubclassingController.Instantiate(SubclassingController.GetClassName(typeof(T))); - - /// - /// Creates a reference to an existing ParseObject for use in creating associations between - /// ParseObjects. Calling on this object will return - /// false until has been called. - /// No network request will be made. - /// - /// The object id for the referenced object. - /// A ParseObject without data. - public static T CreateWithoutData(string objectId) where T : ParseObject => (T) CreateWithoutData(SubclassingController.GetClassName(typeof(T)), objectId); - - // TODO (hallucinogen): add unit test - internal static T FromState(IObjectState state, string defaultClassName) where T : ParseObject - { - T obj = (T) CreateWithoutData(state.ClassName ?? defaultClassName, state.ObjectId); - obj.HandleFetchResult(state); - return obj; - } - - #endregion - - private static string GetFieldForPropertyName(string className, string propertyName) => SubclassingController.GetPropertyMappings(className).TryGetValue(propertyName, out string fieldName) ? fieldName : fieldName; - - /// - /// Sets the value of a property based upon its associated ParseFieldName attribute. - /// - /// The new value. - /// The name of the property. - /// The type for the property. - protected void SetProperty(T value, [CallerMemberName] string propertyName = null) => this[GetFieldForPropertyName(ClassName, propertyName)] = value; - - /// - /// Gets a relation for a property based upon its associated ParseFieldName attribute. - /// - /// The ParseRelation for the property. - /// The name of the property. - /// The ParseObject subclass type of the ParseRelation. - protected ParseRelation GetRelationProperty([CallerMemberName] string propertyName = null) where T : ParseObject => GetRelation(GetFieldForPropertyName(ClassName, propertyName)); - - /// - /// Gets the value of a property based upon its associated ParseFieldName attribute. - /// - /// The value of the property. - /// The name of the property. - /// The return type of the property. - protected T GetProperty([CallerMemberName] string propertyName = null) => GetProperty(default(T), propertyName); - - /// - /// Gets the value of a property based upon its associated ParseFieldName attribute. - /// - /// The value of the property. - /// The value to return if the property is not present on the ParseObject. - /// The name of the property. - /// The return type of the property. - protected T GetProperty(T defaultValue, [CallerMemberName] string propertyName = null) => TryGetValue(GetFieldForPropertyName(ClassName, propertyName), out T result) ? result : defaultValue; - - /// - /// Allows subclasses to set values for non-pointer construction. - /// - internal virtual void SetDefaultValues() { } - - /// - /// Registers a custom subclass type with the Parse SDK, enabling strong-typing of those ParseObjects whenever - /// they appear. Subclasses must specify the ParseClassName attribute, have a default constructor, and properties - /// backed by ParseObject fields should have ParseFieldName attributes supplied. - /// - /// The ParseObject subclass type to register. - public static void RegisterSubclass() where T : ParseObject, new() => SubclassingController.RegisterSubclass(typeof(T)); - - /// - /// Registers a custom subclass type with the Parse SDK, enabling strong-typing of those ParseObjects whenever - /// they appear. Subclasses must specify the ParseClassName attribute, have a default constructor, and properties - /// backed by ParseObject fields should have ParseFieldName attributes supplied. - /// - /// The ParseObject subclass type to register. - public static void RegisterSubclass(Type type) { if (typeof(ParseObject).IsAssignableFrom(type)) SubclassingController.RegisterSubclass(type); } - - internal static void UnregisterSubclass() where T : ParseObject, new() => SubclassingController.UnregisterSubclass(typeof(T)); - internal static void UnregisterSubclass(Type type) { if (typeof(ParseObject).IsAssignableFrom(type)) SubclassingController.UnregisterSubclass(type); } - - - - /// - /// Clears any changes to this object made since the last call to . - /// - public void Revert() - { - lock (mutex) - { - bool wasDirty = CurrentOperations.Count > 0; - if (wasDirty) - { - CurrentOperations.Clear(); - RebuildEstimatedData(); - OnPropertyChanged("IsDirty"); - } - } - } - - internal virtual void HandleFetchResult(IObjectState serverState) - { - lock (mutex) - { - MergeFromServer(serverState); - } - } - - internal void HandleFailedSave(IDictionary operationsBeforeSave) - { - lock (mutex) - { - var opNode = operationSetQueue.Find(operationsBeforeSave); - var nextOperations = opNode.Next.Value; - bool wasDirty = nextOperations.Count > 0; - operationSetQueue.Remove(opNode); - // Merge the data from the failed save into the next save. - foreach (var pair in operationsBeforeSave) - { - var operation1 = pair.Value; - nextOperations.TryGetValue(pair.Key, out IParseFieldOperation operation2); - if (operation2 != null) - operation2 = operation2.MergeWithPrevious(operation1); - else - operation2 = operation1; - nextOperations[pair.Key] = operation2; - } - if (!wasDirty && nextOperations == CurrentOperations && operationsBeforeSave.Count > 0) - OnPropertyChanged("IsDirty"); - } - } - - internal virtual void HandleSave(IObjectState serverState) - { - lock (mutex) - { - var operationsBeforeSave = operationSetQueue.First.Value; - operationSetQueue.RemoveFirst(); - - // Merge the data from the save and the data from the server into serverData. - MutateState(mutableClone => mutableClone.Apply(operationsBeforeSave)); - MergeFromServer(serverState); - } - } - - internal virtual void MergeFromServer(IObjectState serverState) - { - // Make a new serverData with fetched values. - var newServerData = serverState.ToDictionary(t => t.Key, t => t.Value); - - lock (mutex) - { - // Trigger handler based on serverState - if (serverState.ObjectId != null) - { - // If the objectId is being merged in, consider this object to be fetched. - hasBeenFetched = true; - OnPropertyChanged("IsDataAvailable"); - } - if (serverState.UpdatedAt != null) - OnPropertyChanged("UpdatedAt"); - if (serverState.CreatedAt != null) - OnPropertyChanged("CreatedAt"); - - // We cache the fetched object because subsequent Save operation might flush - // the fetched objects into Pointers. - IDictionary fetchedObject = CollectFetchedObjects(); - - foreach (var pair in serverState) - { - var value = pair.Value; - if (value is ParseObject) - { - // Resolve fetched object. - var parseObject = value as ParseObject; - if (fetchedObject.ContainsKey(parseObject.ObjectId)) - value = fetchedObject[parseObject.ObjectId]; - } - newServerData[pair.Key] = value; - } - - IsDirty = false; - serverState = serverState.MutatedClone(mutableClone => mutableClone.ServerData = newServerData); - MutateState(mutableClone => mutableClone.Apply(serverState)); - } - } - - internal void MergeFromObject(ParseObject other) - { - // If they point to the same instance, we don't need to merge - lock (mutex) - if (this == other) - return; - - // Clear out any changes on this object. - if (operationSetQueue.Count != 1) - throw new InvalidOperationException("Attempt to MergeFromObject during save."); - operationSetQueue.Clear(); - foreach (var operationSet in other.operationSetQueue) - operationSetQueue.AddLast(operationSet.ToDictionary(entry => entry.Key, entry => entry.Value)); - - lock (mutex) - state = other.State; - RebuildEstimatedData(); - } - - private bool HasDirtyChildren - { - get - { - lock (mutex) - return FindUnsavedChildren().FirstOrDefault() != null; - } - } - - /// - /// Flattens dictionaries and lists into a single enumerable of all contained objects - /// that can then be queried over. - /// - /// The root of the traversal - /// Whether to traverse into ParseObjects' children - /// Whether to include the root in the result - /// - internal static IEnumerable DeepTraversal(object root, bool traverseParseObjects = false, bool yieldRoot = false) - { - var items = DeepTraversalInternal(root, traverseParseObjects, new HashSet(new IdentityEqualityComparer())); - if (yieldRoot) - return new[] { root }.Concat(items); - else - return items; - } - - private static IEnumerable DeepTraversalInternal(object root, bool traverseParseObjects, ICollection seen) - { - seen.Add(root); - var itemsToVisit = isCompiledByIL2CPP ? (System.Collections.IEnumerable) null : (IEnumerable) null; - var dict = Conversion.As>(root); - if (dict != null) - { - itemsToVisit = dict.Values; - } - else - { - var list = Conversion.As>(root); - if (list != null) - { - itemsToVisit = list; - } - else if (traverseParseObjects) - { - if (root is ParseObject obj) - { - itemsToVisit = obj.Keys.ToList().Select(k => obj[k]); - } - } - } - if (itemsToVisit != null) - { - foreach (var i in itemsToVisit) - { - if (!seen.Contains(i)) - { - yield return i; - var children = DeepTraversalInternal(i, traverseParseObjects, seen); - foreach (var child in children) - { - yield return child; - } - } - } - } - } - - private IEnumerable FindUnsavedChildren() => DeepTraversal(estimatedData).OfType().Where(o => o.IsDirty); - - /// - /// Deep traversal of this object to grab a copy of any object referenced by this object. - /// These instances may have already been fetched, and we don't want to lose their data when - /// refreshing or saving. - /// - /// Map of objectId to ParseObject which have been fetched. - private IDictionary CollectFetchedObjects() => DeepTraversal(estimatedData).OfType().Where(o => o.ObjectId != null && o.IsDataAvailable).GroupBy(o => o.ObjectId).ToDictionary(group => group.Key, group => group.Last()); - - internal static IDictionary ToJSONObjectForSaving(IDictionary operations) - { - var result = new Dictionary(); - foreach (var pair in operations) - result[pair.Key] = PointerOrLocalIdEncoder.Instance.Encode(pair.Value); - return result; - } - - internal IDictionary ServerDataToJSONObjectForSerialization() => PointerOrLocalIdEncoder.Instance.Encode(state.ToDictionary(t => t.Key, t => t.Value)) as IDictionary; - - #region Save Object(s) - - /// - /// Pushes new operations onto the queue and returns the current set of operations. - /// - internal IDictionary StartSave() - { - lock (mutex) - { - var currentOperations = CurrentOperations; - operationSetQueue.AddLast(new Dictionary()); - OnPropertyChanged("IsDirty"); - return currentOperations; - } - } - - protected virtual Task SaveAsync(Task toAwait, CancellationToken cancellationToken) - { - IDictionary currentOperations = null; - if (!IsDirty) - return Task.FromResult(0); - - Task deepSaveTask; - string sessionToken; - lock (mutex) - { - // Get the JSON representation of the object. - currentOperations = StartSave(); - - sessionToken = ParseUser.CurrentSessionToken; - - deepSaveTask = DeepSaveAsync(estimatedData, sessionToken, cancellationToken); - } - - return deepSaveTask.OnSuccess(_ => toAwait).Unwrap().OnSuccess(_ => ObjectController.SaveAsync(state, currentOperations, sessionToken, cancellationToken)).Unwrap().ContinueWith(t => - { - if (t.IsFaulted || t.IsCanceled) - HandleFailedSave(currentOperations); - else - HandleSave(t.Result); - return t; - }).Unwrap(); - } - - /// - /// Saves this object to the server. - /// - public Task SaveAsync() => SaveAsync(CancellationToken.None); - - /// - /// Saves this object to the server. - /// - /// The cancellation token. - public Task SaveAsync(CancellationToken cancellationToken) => taskQueue.Enqueue(toAwait => SaveAsync(toAwait, cancellationToken), cancellationToken); - - internal virtual Task FetchAsyncInternal(Task toAwait, CancellationToken cancellationToken) => toAwait.OnSuccess(_ => ObjectId == null ? throw new InvalidOperationException("Cannot refresh an object that hasn't been saved to the server.") : ObjectController.FetchAsync(state, ParseUser.CurrentSessionToken, cancellationToken)).Unwrap().OnSuccess(t => - { - HandleFetchResult(t.Result); - return this; - }); - - private static Task DeepSaveAsync(object obj, string sessionToken, CancellationToken cancellationToken) - { - var objects = new List(); - CollectDirtyChildren(obj, objects); - - var uniqueObjects = new HashSet(objects, new IdentityEqualityComparer()); - - var saveDirtyFileTasks = DeepTraversal(obj, true).OfType().Where(f => f.IsDirty).Select(f => f.SaveAsync(cancellationToken)).ToList(); - - return Task.WhenAll(saveDirtyFileTasks).OnSuccess(_ => - { - IEnumerable remaining = new List(uniqueObjects); - return InternalExtensions.WhileAsync(() => Task.FromResult(remaining.Any()), () => - { - // Partition the objects into two sets: those that can be saved immediately, - // and those that rely on other objects to be created first. - var current = (from item in remaining where item.CanBeSerialized select item).ToList(); - var nextBatch = (from item in remaining where !item.CanBeSerialized select item).ToList(); - remaining = nextBatch; - - if (current.Count == 0) - { - // We do cycle-detection when building the list of objects passed to this - // function, so this should never get called. But we should check for it - // anyway, so that we get an exception instead of an infinite loop. - throw new InvalidOperationException( - "Unable to save a ParseObject with a relation to a cycle."); - } - - // Save all of the objects in current. - return ParseObject.EnqueueForAll(current, toAwait => - { - return toAwait.OnSuccess(__ => - { - var states = (from item in current - select item.state).ToList(); - var operationsList = (from item in current - select item.StartSave()).ToList(); - - var saveTasks = ObjectController.SaveAllAsync(states, - operationsList, - sessionToken, - cancellationToken); - - return Task.WhenAll(saveTasks).ContinueWith(t => - { - if (t.IsFaulted || t.IsCanceled) - { - foreach (var pair in current.Zip(operationsList, (item, ops) => new { item, ops })) - { - pair.item.HandleFailedSave(pair.ops); - } - } - else - { - var serverStates = t.Result; - foreach (var pair in current.Zip(serverStates, (item, state) => new { item, state })) - { - pair.item.HandleSave(pair.state); - } - } - cancellationToken.ThrowIfCancellationRequested(); - return t; - }).Unwrap(); - }).Unwrap().OnSuccess(t => (object) null); - }, cancellationToken); - }); - }).Unwrap(); - } - - /// - /// Saves each object in the provided list. - /// - /// The objects to save. - public static Task SaveAllAsync(IEnumerable objects) where T : ParseObject => SaveAllAsync(objects, CancellationToken.None); - - /// - /// Saves each object in the provided list. - /// - /// The objects to save. - /// The cancellation token. - public static Task SaveAllAsync( - IEnumerable objects, CancellationToken cancellationToken) where T : ParseObject - { - return DeepSaveAsync(objects.ToList(), ParseUser.CurrentSessionToken, cancellationToken); - } - - #endregion - - #region Fetch Object(s) - - /// - /// Fetches this object with the data from the server. - /// - /// The cancellation token. - internal Task FetchAsyncInternal(CancellationToken cancellationToken) - { - return taskQueue.Enqueue(toAwait => FetchAsyncInternal(toAwait, cancellationToken), - cancellationToken); - } - - internal Task FetchIfNeededAsyncInternal( - Task toAwait, CancellationToken cancellationToken) - { - if (!IsDataAvailable) - { - return FetchAsyncInternal(toAwait, cancellationToken); - } - return Task.FromResult(this); - } - - /// - /// If this ParseObject has not been fetched (i.e. returns - /// false), fetches this object with the data from the server. - /// - /// The cancellation token. - internal Task FetchIfNeededAsyncInternal(CancellationToken cancellationToken) - { - return taskQueue.Enqueue(toAwait => FetchIfNeededAsyncInternal(toAwait, cancellationToken), - cancellationToken); - } - - /// - /// Fetches all of the objects that don't have data in the provided list. - /// - /// The list passed in for convenience. - public static Task> FetchAllIfNeededAsync( - IEnumerable objects) where T : ParseObject - { - return FetchAllIfNeededAsync(objects, CancellationToken.None); - } - - /// - /// Fetches all of the objects that don't have data in the provided list. - /// - /// The objects to fetch. - /// The cancellation token. - /// The list passed in for convenience. - public static Task> FetchAllIfNeededAsync( - IEnumerable objects, CancellationToken cancellationToken) where T : ParseObject - { - return ParseObject.EnqueueForAll(objects.Cast(), (Task toAwait) => - { - return FetchAllInternalAsync(objects, false, toAwait, cancellationToken); - }, cancellationToken); - } - - /// - /// Fetches all of the objects in the provided list. - /// - /// The objects to fetch. - /// The list passed in for convenience. - public static Task> FetchAllAsync( - IEnumerable objects) where T : ParseObject - { - return FetchAllAsync(objects, CancellationToken.None); - } - - /// - /// Fetches all of the objects in the provided list. - /// - /// The objects to fetch. - /// The cancellation token. - /// The list passed in for convenience. - public static Task> FetchAllAsync( - IEnumerable objects, CancellationToken cancellationToken) where T : ParseObject - { - return ParseObject.EnqueueForAll(objects.Cast(), (Task toAwait) => - { - return FetchAllInternalAsync(objects, true, toAwait, cancellationToken); - }, cancellationToken); - } - - /// - /// Fetches all of the objects in the list. - /// - /// The objects to fetch. - /// If false, only objects without data will be fetched. - /// A task to await before starting. - /// The cancellation token. - /// The list passed in for convenience. - private static Task> FetchAllInternalAsync( - IEnumerable objects, bool force, Task toAwait, CancellationToken cancellationToken) where T : ParseObject - { - return toAwait.OnSuccess(_ => - { - if (objects.Any(obj => { return obj.state.ObjectId == null; })) - { - throw new InvalidOperationException("You cannot fetch objects that haven't already been saved."); - } - - var objectsToFetch = (from obj in objects - where force || !obj.IsDataAvailable - select obj).ToList(); - - if (objectsToFetch.Count == 0) - { - return Task.FromResult(objects); - } - - // Do one Find for each class. - var findsByClass = - (from obj in objectsToFetch - group obj.ObjectId by obj.ClassName into classGroup - where classGroup.Count() > 0 - select new - { - ClassName = classGroup.Key, - FindTask = new ParseQuery(classGroup.Key) - .WhereContainedIn("objectId", classGroup) - .FindAsync(cancellationToken) - }).ToDictionary(pair => pair.ClassName, pair => pair.FindTask); - - // Wait for all the Finds to complete. - return Task.WhenAll(findsByClass.Values.ToList()).OnSuccess(__ => - { - if (cancellationToken.IsCancellationRequested) - { - return objects; - } - - // Merge the data from the Finds into the input objects. - var pairs = from obj in objectsToFetch - from result in findsByClass[obj.ClassName].Result - where result.ObjectId == obj.ObjectId - select new { obj, result }; - foreach (var pair in pairs) - { - pair.obj.MergeFromObject(pair.result); - pair.obj.hasBeenFetched = true; - } - - return objects; - }); - }).Unwrap(); - } - - #endregion - - #region Delete Object - - internal Task DeleteAsync(Task toAwait, CancellationToken cancellationToken) - { - if (ObjectId == null) - { - return Task.FromResult(0); - } - - string sessionToken = ParseUser.CurrentSessionToken; - - return toAwait.OnSuccess(_ => - { - return ObjectController.DeleteAsync(State, sessionToken, cancellationToken); - }).Unwrap().OnSuccess(_ => IsDirty = true); - } - - /// - /// Deletes this object on the server. - /// - public Task DeleteAsync() - { - return DeleteAsync(CancellationToken.None); - } - - /// - /// Deletes this object on the server. - /// - /// The cancellation token. - public Task DeleteAsync(CancellationToken cancellationToken) - { - return taskQueue.Enqueue(toAwait => DeleteAsync(toAwait, cancellationToken), - cancellationToken); - } - - /// - /// Deletes each object in the provided list. - /// - /// The objects to delete. - public static Task DeleteAllAsync(IEnumerable objects) where T : ParseObject - { - return DeleteAllAsync(objects, CancellationToken.None); - } - - /// - /// Deletes each object in the provided list. - /// - /// The objects to delete. - /// The cancellation token. - public static Task DeleteAllAsync( - IEnumerable objects, CancellationToken cancellationToken) where T : ParseObject - { - var uniqueObjects = new HashSet(objects.OfType().ToList(), - new IdentityEqualityComparer()); - - return ParseObject.EnqueueForAll(uniqueObjects, toAwait => - { - var states = uniqueObjects.Select(t => t.state).ToList(); - return toAwait.OnSuccess(_ => - { - var deleteTasks = ObjectController.DeleteAllAsync(states, - ParseUser.CurrentSessionToken, - cancellationToken); - - return Task.WhenAll(deleteTasks); - }).Unwrap().OnSuccess(t => - { - // Dirty all objects in memory. - foreach (var obj in uniqueObjects) - { - obj.IsDirty = true; - } - - return (object) null; - }); - }, cancellationToken); - } - - #endregion - - private static void CollectDirtyChildren(object node, - IList dirtyChildren, - ICollection seen, - ICollection seenNew) - { - foreach (var obj in DeepTraversal(node).OfType()) - { - ICollection scopedSeenNew; - // Check for cycles of new objects. Any such cycle means it will be impossible to save - // this collection of objects, so throw an exception. - if (obj.ObjectId != null) - { - scopedSeenNew = new HashSet(new IdentityEqualityComparer()); - } - else - { - if (seenNew.Contains(obj)) - { - throw new InvalidOperationException("Found a circular dependency while saving"); - } - scopedSeenNew = new HashSet(seenNew, new IdentityEqualityComparer()) { obj }; - } - - // Check for cycles of any object. If this occurs, then there's no problem, but - // we shouldn't recurse any deeper, because it would be an infinite recursion. - if (seen.Contains(obj)) - { - return; - } - seen.Add(obj); - - // Recurse into this object's children looking for dirty children. - // We only need to look at the child object's current estimated data, - // because that's the only data that might need to be saved now. - CollectDirtyChildren(obj.estimatedData, dirtyChildren, seen, scopedSeenNew); - - if (obj.CheckIsDirty(false)) - { - dirtyChildren.Add(obj); - } - } - } - - /// - /// Helper version of CollectDirtyChildren so that callers don't have to add the internally - /// used parameters. - /// - private static void CollectDirtyChildren(object node, IList dirtyChildren) - { - CollectDirtyChildren(node, - dirtyChildren, - new HashSet(new IdentityEqualityComparer()), - new HashSet(new IdentityEqualityComparer())); - } - - /// - /// Returns true if the given object can be serialized for saving as a value - /// that is pointed to by a ParseObject. - /// - private static bool CanBeSerializedAsValue(object value) - { - return DeepTraversal(value, yieldRoot: true) - .OfType() - .All(o => o.ObjectId != null); - } - - private bool CanBeSerialized - { - get - { - // This method is only used for batching sets of objects for saveAll - // and when saving children automatically. Since it's only used to - // determine whether or not save should be called on them, it only - // needs to examine their current values, so we use estimatedData. - lock (mutex) - { - return CanBeSerializedAsValue(estimatedData); - } - } - } - - /// - /// Adds a task to the queue for all of the given objects. - /// - private static Task EnqueueForAll(IEnumerable objects, - Func> taskStart, CancellationToken cancellationToken) - { - // The task that will be complete when all of the child queues indicate they're ready to start. - var readyToStart = new TaskCompletionSource(); - - // First, we need to lock the mutex for the queue for every object. We have to hold this - // from at least when taskStart() is called to when obj.taskQueue enqueue is called, so - // that saves actually get executed in the order they were setup by taskStart(). - // The locks have to be sorted so that we always acquire them in the same order. - // Otherwise, there's some risk of deadlock. - var lockSet = new LockSet(objects.Select(o => o.taskQueue.Mutex)); - - lockSet.Enter(); - try - { - - // The task produced by taskStart. By running this immediately, we allow everything prior - // to toAwait to run before waiting for all of the queues on all of the objects. - Task fullTask = taskStart(readyToStart.Task); - - // Add fullTask to each of the objects' queues. - var childTasks = new List(); - foreach (ParseObject obj in objects) - { - obj.taskQueue.Enqueue((Task task) => - { - childTasks.Add(task); - return fullTask; - }, cancellationToken); - } - - // When all of the objects' queues are ready, signal fullTask that it's ready to go on. - Task.WhenAll(childTasks.ToArray()).ContinueWith((Task task) => - { - readyToStart.SetResult(null); - }); - - return fullTask; - } - finally - { - lockSet.Exit(); - } - } - - /// - /// Removes a key from the object's data if it exists. - /// - /// The key to remove. - public virtual void Remove(string key) - { - lock (mutex) - { - CheckKeyIsMutable(key); - - PerformOperation(key, ParseDeleteOperation.Instance); - } - } - - - private void ApplyOperations(IDictionary operations, - IDictionary map) - { - lock (mutex) - { - foreach (var pair in operations) - { - map.TryGetValue(pair.Key, out object oldValue); - var newValue = pair.Value.Apply(oldValue, pair.Key); - if (newValue != ParseDeleteOperation.DeleteToken) - { - map[pair.Key] = newValue; - } - else - { - map.Remove(pair.Key); - } - } - } - } - - /// - /// Regenerates the estimatedData map from the serverData and operations. - /// - internal void RebuildEstimatedData() - { - lock (mutex) - { - estimatedData.Clear(); - foreach (var item in state) - { - estimatedData.Add(item); - } - foreach (var operations in operationSetQueue) - { - ApplyOperations(operations, estimatedData); - } - // We've just applied a bunch of operations to estimatedData which - // may have changed all of its keys. Notify of all keys and properties - // mapped to keys being changed. - OnFieldsChanged(null); - } - } - - /// - /// PerformOperation is like setting a value at an index, but instead of - /// just taking a new value, it takes a ParseFieldOperation that modifies the value. - /// - internal void PerformOperation(string key, IParseFieldOperation operation) - { - lock (mutex) - { - estimatedData.TryGetValue(key, out object oldValue); - object newValue = operation.Apply(oldValue, key); - if (newValue != ParseDeleteOperation.DeleteToken) - { - estimatedData[key] = newValue; - } - else - { - estimatedData.Remove(key); - } - - bool wasDirty = CurrentOperations.Count > 0; - CurrentOperations.TryGetValue(key, out IParseFieldOperation oldOperation); - var newOperation = operation.MergeWithPrevious(oldOperation); - CurrentOperations[key] = newOperation; - if (!wasDirty) - { - OnPropertyChanged("IsDirty"); - } - - OnFieldsChanged(new[] { key }); - } - } - - /// - /// Override to run validations on key/value pairs. Make sure to still - /// call the base version. - /// - internal virtual void OnSettingValue(ref string key, ref object value) - { - if (key == null) - { - throw new ArgumentNullException("key"); - } - } - - /// - /// Gets or sets a value on the object. It is recommended to name - /// keys in partialCamelCaseLikeThis. - /// - /// The key for the object. Keys must be alphanumeric plus underscore - /// and start with a letter. - /// The property is - /// retrieved and is not found. - /// The value for the key. - virtual public object this[string key] - { - get - { - lock (mutex) - { - CheckGetAccess(key); - - var value = estimatedData[key]; - - // A relation may be deserialized without a parent or key. Either way, - // make sure it's consistent. - if (value is ParseRelationBase relation) - { - relation.EnsureParentAndKey(this, key); - } - - return value; - } - } - set - { - lock (mutex) - { - CheckKeyIsMutable(key); - - Set(key, value); - } - } - } - - /// - /// Perform Set internally which is not gated by mutability check. - /// - /// key for the object. - /// the value for the key. - internal void Set(string key, object value) - { - lock (mutex) - { - OnSettingValue(ref key, ref value); - - if (!ParseEncoder.IsValidType(value)) - { - throw new ArgumentException("Invalid type for value: " + value.GetType().ToString()); - } - - PerformOperation(key, new ParseSetOperation(value)); - } - } - - internal void SetIfDifferent(string key, T value) - { - bool hasCurrent = TryGetValue(key, out T current); - if (value == null) - { - if (hasCurrent) - { - PerformOperation(key, ParseDeleteOperation.Instance); - } - return; - } - if (!hasCurrent || !value.Equals(current)) - { - Set(key, value); - } - } - - #region Atomic Increment - - /// - /// Atomically increments the given key by 1. - /// - /// The key to increment. - public void Increment(string key) - { - Increment(key, 1); - } - - /// - /// Atomically increments the given key by the given number. - /// - /// The key to increment. - /// The amount to increment by. - public void Increment(string key, long amount) - { - lock (mutex) - { - CheckKeyIsMutable(key); - - PerformOperation(key, new ParseIncrementOperation(amount)); - } - } - - /// - /// Atomically increments the given key by the given number. - /// - /// The key to increment. - /// The amount to increment by. - public void Increment(string key, double amount) - { - lock (mutex) - { - CheckKeyIsMutable(key); - - PerformOperation(key, new ParseIncrementOperation(amount)); - } - } - - #endregion - - /// - /// Atomically adds an object to the end of the list associated with the given key. - /// - /// The key. - /// The object to add. - public void AddToList(string key, object value) - { - AddRangeToList(key, new[] { value }); - } - - /// - /// Atomically adds objects to the end of the list associated with the given key. - /// - /// The key. - /// The objects to add. - public void AddRangeToList(string key, IEnumerable values) - { - lock (mutex) - { - CheckKeyIsMutable(key); - - PerformOperation(key, new ParseAddOperation(values.Cast())); - } - } - - /// - /// Atomically adds an object to the end of the list associated with the given key, - /// only if it is not already present in the list. The position of the insert is not - /// guaranteed. - /// - /// The key. - /// The object to add. - public void AddUniqueToList(string key, object value) - { - AddRangeUniqueToList(key, new object[] { value }); - } - - /// - /// Atomically adds objects to the end of the list associated with the given key, - /// only if they are not already present in the list. The position of the inserts are not - /// guaranteed. - /// - /// The key. - /// The objects to add. - public void AddRangeUniqueToList(string key, IEnumerable values) - { - lock (mutex) - { - CheckKeyIsMutable(key); - - PerformOperation(key, new ParseAddUniqueOperation(values.Cast())); - } - } - - /// - /// Atomically removes all instances of the objects in - /// from the list associated with the given key. - /// - /// The key. - /// The objects to remove. - public void RemoveAllFromList(string key, IEnumerable values) - { - lock (mutex) - { - CheckKeyIsMutable(key); - - PerformOperation(key, new ParseRemoveOperation(values.Cast())); - } - } - - /// - /// Returns whether this object has a particular key. - /// - /// The key to check for - public bool ContainsKey(string key) - { - lock (mutex) - { - return estimatedData.ContainsKey(key); - } - } - - /// - /// Gets a value for the key of a particular type. - /// The type to convert the value to. Supported types are - /// ParseObject and its descendents, Parse types such as ParseRelation and ParseGeopoint, - /// primitive types,IList<T>, IDictionary<string, T>, and strings. - /// The key of the element to get. - /// The property is - /// retrieved and is not found. - /// - public T Get(string key) - { - return Conversion.To(this[key]); - } - - /// - /// Access or create a Relation value for a key. - /// - /// The type of object to create a relation for. - /// The key for the relation field. - /// A ParseRelation for the key. - public ParseRelation GetRelation(string key) where T : ParseObject - { - // All the sanity checking is done when add or remove is called. - TryGetValue(key, out ParseRelation relation); - return relation ?? new ParseRelation(this, key); - } - - /// - /// Populates result with the value for the key, if possible. - /// - /// The desired type for the value. - /// The key to retrieve a value for. - /// The value for the given key, converted to the - /// requested type, or null if unsuccessful. - /// true if the lookup and conversion succeeded, otherwise - /// false. - public bool TryGetValue(string key, out T result) - { - lock (mutex) - { - if (ContainsKey(key)) - { - try - { - var temp = Conversion.To(this[key]); - result = temp; - return true; - } - catch - { - result = default(T); - return false; - } - } - result = default(T); - return false; - } - } - - /// - /// Gets whether the ParseObject has been fetched. - /// - public bool IsDataAvailable - { - get - { - lock (mutex) - { - return hasBeenFetched; - } - } - } - - private bool CheckIsDataAvailable(string key) - { - lock (mutex) - { - return IsDataAvailable || estimatedData.ContainsKey(key); - } - } - - private void CheckGetAccess(string key) - { - lock (mutex) - { - if (!CheckIsDataAvailable(key)) - { - throw new InvalidOperationException( - "ParseObject has no data for this key. Call FetchIfNeededAsync() to get the data."); - } - } - } - - private void CheckKeyIsMutable(string key) - { - if (!IsKeyMutable(key)) - { - throw new InvalidOperationException( - "Cannot change the `" + key + "` property of a `" + ClassName + "` object."); - } - } - - protected virtual bool IsKeyMutable(string key) - { - return true; - } - - /// - /// A helper function for checking whether two ParseObjects point to - /// the same object in the cloud. - /// - public bool HasSameId(ParseObject other) - { - lock (mutex) - { - return other != null && - Equals(ClassName, other.ClassName) && - Equals(ObjectId, other.ObjectId); - } - } - - internal IDictionary CurrentOperations - { - get - { - lock (mutex) - { - return operationSetQueue.Last.Value; - } - } - } - - /// - /// Gets a set view of the keys contained in this object. This does not include createdAt, - /// updatedAt, or objectId. It does include things like username and ACL. - /// - public ICollection Keys - { - get - { - lock (mutex) - { - return estimatedData.Keys; - } - } - } - - /// - /// Gets or sets the ParseACL governing this object. - /// - [ParseFieldName("ACL")] - public ParseACL ACL - { - get { return GetProperty(null, "ACL"); } - set { SetProperty(value, "ACL"); } - } - - /// - /// Returns true if this object was created by the Parse server when the - /// object might have already been there (e.g. in the case of a Facebook - /// login) - /// -#if !UNITY - public -#else - internal -#endif - bool IsNew - { - get - { - return state.IsNew; - } -#if !UNITY - internal -#endif - set - { - MutateState(mutableClone => - { - mutableClone.IsNew = value; - }); - OnPropertyChanged("IsNew"); - } - } - - /// - /// Gets the last time this object was updated as the server sees it, so that if you make changes - /// to a ParseObject, then wait a while, and then call , the updated time - /// will be the time of the call rather than the time the object was - /// changed locally. - /// - [ParseFieldName("updatedAt")] - public DateTime? UpdatedAt - { - get - { - return state.UpdatedAt; - } - } - - /// - /// Gets the first time this object was saved as the server sees it, so that if you create a - /// ParseObject, then wait a while, and then call , the - /// creation time will be the time of the first call rather than - /// the time the object was created locally. - /// - [ParseFieldName("createdAt")] - public DateTime? CreatedAt - { - get - { - return state.CreatedAt; - } - } - - /// - /// Indicates whether this ParseObject has unsaved changes. - /// - public bool IsDirty - { - get - { - lock (mutex) - { return CheckIsDirty(true); } - } - internal set - { - lock (mutex) - { - dirty = value; - OnPropertyChanged("IsDirty"); - } - } - } - - /// - /// Indicates whether key is unsaved for this ParseObject. - /// - /// The key to check for. - /// true if the key has been altered and not saved yet, otherwise - /// false. - public bool IsKeyDirty(string key) - { - lock (mutex) - { - return CurrentOperations.ContainsKey(key); - } - } - - private bool CheckIsDirty(bool considerChildren) - { - lock (mutex) - { - return (dirty || CurrentOperations.Count > 0 || (considerChildren && HasDirtyChildren)); - } - } - - /// - /// Gets or sets the object id. An object id is assigned as soon as an object is - /// saved to the server. The combination of a and an - /// uniquely identifies an object in your application. - /// - [ParseFieldName("objectId")] - public string ObjectId - { - get - { - return state.ObjectId; - } - set - { - IsDirty = true; - SetObjectIdInternal(value); - } - } - /// - /// Sets the objectId without marking dirty. - /// - /// The new objectId - private void SetObjectIdInternal(string objectId) - { - lock (mutex) - { - MutateState(mutableClone => - { - mutableClone.ObjectId = objectId; - }); - OnPropertyChanged("ObjectId"); - } - } - - /// - /// Gets the class name for the ParseObject. - /// - public string ClassName - { - get - { - return state.ClassName; - } - } - - /// - /// Adds a value for the given key, throwing an Exception if the key - /// already has a value. - /// - /// - /// This allows you to use collection initialization syntax when creating ParseObjects, - /// such as: - /// - /// var obj = new ParseObject("MyType") - /// { - /// {"name", "foo"}, - /// {"count", 10}, - /// {"found", false} - /// }; - /// - /// - /// The key for which a value should be set. - /// The value for the key. - public void Add(string key, object value) - { - lock (mutex) - { - if (this.ContainsKey(key)) - { - throw new ArgumentException("Key already exists", key); - } - this[key] = value; - } - } - - IEnumerator> IEnumerable> - .GetEnumerator() - { - lock (mutex) - { - return estimatedData.GetEnumerator(); - } - } - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() - { - lock (mutex) - { - return ((IEnumerable>) this).GetEnumerator(); - } - } - - /// - /// Gets a for the type of object specified by - /// - /// - /// The class name of the object. - /// A new . - public static ParseQuery GetQuery(string className) - { - // Since we can't return a ParseQuery (due to strong-typing with - // generics), we'll require you to go through subclasses. This is a better - // experience anyway, especially with LINQ integration, since you'll get - // strongly-typed queries and compile-time checking of property names and - // types. - if (SubclassingController.GetType(className) != null) - { - throw new ArgumentException( - "Use the class-specific query properties for class " + className, "className"); - } - return new ParseQuery(className); - } - - /// - /// Raises change notifications for all properties associated with the given - /// field names. If fieldNames is null, this will notify for all known field-linked - /// properties (e.g. this happens when we recalculate all estimated data from scratch) - /// - protected void OnFieldsChanged(IEnumerable fieldNames) - { - var mappings = SubclassingController.GetPropertyMappings(ClassName); - IEnumerable properties; - - if (fieldNames != null && mappings != null) - { - properties = from m in mappings - join f in fieldNames on m.Value equals f - select m.Key; - } - else if (mappings != null) - { - properties = mappings.Keys; - } - else - { - properties = Enumerable.Empty(); - } - - foreach (var property in properties) - { - OnPropertyChanged(property); - } - OnPropertyChanged("Item[]"); - } - - /// - /// Raises change notifications for a property. Passing null or the empty string - /// notifies the binding framework that all properties/indexes have changed. - /// Passing "Item[]" tells the binding framework that all indexed values - /// have changed (but not all properties) - /// - protected void OnPropertyChanged( -#if !UNITY -[CallerMemberName] string propertyName = null -#else -string propertyName -#endif -) - { - propertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - - private SynchronizedEventHandler propertyChanged = - new SynchronizedEventHandler(); - /// - /// Occurs when a property value changes. - /// - public event PropertyChangedEventHandler PropertyChanged - { - add - { - propertyChanged.Add(value); - } - remove - { - propertyChanged.Remove(value); - } - } - } -} diff --git a/Parse/Public/ParsePush.cs b/Parse/Public/ParsePush.cs deleted file mode 100644 index 7b97da1c..00000000 --- a/Parse/Public/ParsePush.cs +++ /dev/null @@ -1,556 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Parse.Push.Internal; -using Parse.Core.Internal; -using Parse.Common.Internal; - -namespace Parse -{ - /// - /// A utility class for sending and receiving push notifications. - /// - public partial class ParsePush - { - private object mutex; - private IPushState state; - - /// - /// Creates a push which will target every device. The Data field must be set before calling SendAsync. - /// - public ParsePush() - { - mutex = new object(); - // Default to everyone. - state = new MutablePushState - { - Query = ParseInstallation.Query - }; - } - - #region Properties - - /// - /// An installation query that specifies which installations should receive - /// this push. - /// This should not be used in tandem with Channels. - /// - public ParseQuery Query - { - get { return state.Query; } - set - { - MutateState(s => - { - if (s.Channels != null && value != null && value.GetConstraint("channels") != null) - { - throw new InvalidOperationException("A push may not have both Channels and a Query with a channels constraint"); - } - s.Query = value; - }); - } - } - - /// - /// A short-hand to set a query which only discriminates on the channels to which a device is subscribed. - /// This is shorthand for: - /// - /// - /// var push = new Push(); - /// push.Query = ParseInstallation.Query.WhereKeyContainedIn("channels", channels); - /// - /// - /// This cannot be used in tandem with Query. - /// - public IEnumerable Channels - { - get { return state.Channels; } - set - { - MutateState(s => - { - if (value != null && s.Query != null && s.Query.GetConstraint("channels") != null) - { - throw new InvalidOperationException("A push may not have both Channels and a Query with a channels constraint"); - } - s.Channels = value; - }); - } - } - - /// - /// The time at which this push will expire. This should not be used in tandem with ExpirationInterval. - /// - public DateTime? Expiration - { - get { return state.Expiration; } - set - { - MutateState(s => - { - if (s.ExpirationInterval != null) - { - throw new InvalidOperationException("Cannot set Expiration after setting ExpirationInterval"); - } - s.Expiration = value; - }); - } - } - - /// - /// The time at which this push will be sent. - /// - public DateTime? PushTime - { - get { return state.PushTime; } - set - { - MutateState(s => - { - DateTime now = DateTime.Now; - if (value < now || value > now.AddDays(14)) - { - throw new InvalidOperationException("Cannot set PushTime in the past or more than two weeks later than now"); - } - s.PushTime = value; - }); - } - } - - /// - /// The time from initial schedul when this push will expire. This should not be used in tandem with Expiration. - /// - public TimeSpan? ExpirationInterval - { - get { return state.ExpirationInterval; } - set - { - MutateState(s => - { - if (s.Expiration != null) - { - throw new InvalidOperationException("Cannot set ExpirationInterval after setting Expiration"); - } - s.ExpirationInterval = value; - }); - } - } - - /// - /// The contents of this push. Some keys have special meaning. A full list of pre-defined - /// keys can be found in the Parse Push Guide. The following keys affect WinRT devices. - /// Keys which do not start with x-winrt- can be prefixed with x-winrt- to specify an - /// override only sent to winrt devices. - /// alert: the body of the alert text. - /// title: The title of the text. - /// x-winrt-payload: A full XML payload to be sent to WinRT installations instead of - /// the auto-layout. - /// This should not be used in tandem with Alert. - /// - public IDictionary Data - { - get { return state.Data; } - set - { - MutateState(s => - { - if (s.Alert != null && value != null) - { - throw new InvalidOperationException("A push may not have both an Alert and Data"); - } - s.Data = value; - }); - } - } - - /// - /// A convenience method which sets Data to a dictionary with alert as its only field. Equivalent to - /// - /// - /// Data = new Dictionary<string, object> {{"alert", alert}}; - /// - /// - /// This should not be used in tandem with Data. - /// - public string Alert - { - get { return state.Alert; } - set - { - MutateState(s => - { - if (s.Data != null && value != null) - { - throw new InvalidOperationException("A push may not have both an Alert and Data"); - } - s.Alert = value; - }); - } - } - - #endregion - - internal IDictionary Encode() - { - return ParsePushEncoder.Instance.Encode(state); - } - - private void MutateState(Action func) - { - lock (mutex) - { - state = state.MutatedClone(func); - } - } - - private static IParsePushController PushController - { - get - { - return ParsePushPlugins.Instance.PushController; - } - } - - private static IParsePushChannelsController PushChannelsController - { - get - { - return ParsePushPlugins.Instance.PushChannelsController; - } - } - - #region Sending Push - - /// - /// Request a push to be sent. When this task completes, Parse has successfully acknowledged a request - /// to send push notifications but has not necessarily finished sending all notifications - /// requested. The current status of recent push notifications can be seen in your Push Notifications - /// console on http://parse.com - /// - /// A Task for continuation. - public Task SendAsync() - { - return SendAsync(CancellationToken.None); - } - - /// - /// Request a push to be sent. When this task completes, Parse has successfully acknowledged a request - /// to send push notifications but has not necessarily finished sending all notifications - /// requested. The current status of recent push notifications can be seen in your Push Notifications - /// console on http://parse.com - /// - /// CancellationToken to cancel the current operation. - public Task SendAsync(CancellationToken cancellationToken) - { - return PushController.SendPushNotificationAsync(state, cancellationToken); - } - - /// - /// Pushes a simple message to every device. This is shorthand for: - /// - /// - /// var push = new ParsePush(); - /// push.Data = new Dictionary<string, object>{{"alert", alert}}; - /// return push.SendAsync(); - /// - /// - /// The alert message to send. - public static Task SendAlertAsync(string alert) - { - var push = new ParsePush(); - push.Alert = alert; - return push.SendAsync(); - } - - /// - /// Pushes a simple message to every device subscribed to channel. This is shorthand for: - /// - /// - /// var push = new ParsePush(); - /// push.Channels = new List<string> { channel }; - /// push.Data = new Dictionary<string, object>{{"alert", alert}}; - /// return push.SendAsync(); - /// - /// - /// The alert message to send. - /// An Installation must be subscribed to channel to receive this Push Notification. - public static Task SendAlertAsync(string alert, string channel) - { - var push = new ParsePush(); - push.Channels = new List { channel }; - push.Alert = alert; - return push.SendAsync(); - } - - /// - /// Pushes a simple message to every device subscribed to any of channels. This is shorthand for: - /// - /// - /// var push = new ParsePush(); - /// push.Channels = channels; - /// push.Data = new Dictionary<string, object>{{"alert", alert}}; - /// return push.SendAsync(); - /// - /// - /// The alert message to send. - /// An Installation must be subscribed to any of channels to receive this Push Notification. - public static Task SendAlertAsync(string alert, IEnumerable channels) - { - var push = new ParsePush(); - push.Channels = channels; - push.Alert = alert; - return push.SendAsync(); - } - - /// - /// Pushes a simple message to every device matching the target query. This is shorthand for: - /// - /// - /// var push = new ParsePush(); - /// push.Query = query; - /// push.Data = new Dictionary<string, object>{{"alert", alert}}; - /// return push.SendAsync(); - /// - /// - /// The alert message to send. - /// A query filtering the devices which should receive this Push Notification. - public static Task SendAlertAsync(string alert, ParseQuery query) - { - var push = new ParsePush(); - push.Query = query; - push.Alert = alert; - return push.SendAsync(); - } - - /// - /// Pushes an arbitrary payload to every device. This is shorthand for: - /// - /// - /// var push = new ParsePush(); - /// push.Data = data; - /// return push.SendAsync(); - /// - /// - /// A push payload. See the ParsePush.Data property for more information. - public static Task SendDataAsync(IDictionary data) - { - var push = new ParsePush(); - push.Data = data; - return push.SendAsync(); - } - - /// - /// Pushes an arbitrary payload to every device subscribed to channel. This is shorthand for: - /// - /// - /// var push = new ParsePush(); - /// push.Channels = new List<string> { channel }; - /// push.Data = data; - /// return push.SendAsync(); - /// - /// - /// A push payload. See the ParsePush.Data property for more information. - /// An Installation must be subscribed to channel to receive this Push Notification. - public static Task SendDataAsync(IDictionary data, string channel) - { - var push = new ParsePush(); - push.Channels = new List { channel }; - push.Data = data; - return push.SendAsync(); - } - - /// - /// Pushes an arbitrary payload to every device subscribed to any of channels. This is shorthand for: - /// - /// - /// var push = new ParsePush(); - /// push.Channels = channels; - /// push.Data = data; - /// return push.SendAsync(); - /// - /// - /// A push payload. See the ParsePush.Data property for more information. - /// An Installation must be subscribed to any of channels to receive this Push Notification. - public static Task SendDataAsync(IDictionary data, IEnumerable channels) - { - var push = new ParsePush(); - push.Channels = channels; - push.Data = data; - return push.SendAsync(); - } - - /// - /// Pushes an arbitrary payload to every device matching target. This is shorthand for: - /// - /// - /// var push = new ParsePush(); - /// push.Query = query - /// push.Data = data; - /// return push.SendAsync(); - /// - /// - /// A push payload. See the ParsePush.Data property for more information. - /// A query filtering the devices which should receive this Push Notification. - public static Task SendDataAsync(IDictionary data, ParseQuery query) - { - var push = new ParsePush(); - push.Query = query; - push.Data = data; - return push.SendAsync(); - } - - #endregion - - #region Receiving Push - - /// - /// An event fired when a push notification is received. - /// - public static event EventHandler ParsePushNotificationReceived - { - add - { - parsePushNotificationReceived.Add(value); - } - remove - { - parsePushNotificationReceived.Remove(value); - } - } - - internal static readonly SynchronizedEventHandler parsePushNotificationReceived = new SynchronizedEventHandler(); - - #endregion - - #region Push Subscription - - /// - /// Subscribe the current installation to this channel. This is shorthand for: - /// - /// - /// var installation = ParseInstallation.CurrentInstallation; - /// installation.AddUniqueToList("channels", channel); - /// installation.SaveAsync(); - /// - /// - /// The channel to which this installation should subscribe. - public static Task SubscribeAsync(string channel) - { - return SubscribeAsync(new List { channel }, CancellationToken.None); - } - - /// - /// Subscribe the current installation to this channel. This is shorthand for: - /// - /// - /// var installation = ParseInstallation.CurrentInstallation; - /// installation.AddUniqueToList("channels", channel); - /// installation.SaveAsync(cancellationToken); - /// - /// - /// The channel to which this installation should subscribe. - /// CancellationToken to cancel the current operation. - public static Task SubscribeAsync(string channel, CancellationToken cancellationToken) - { - return SubscribeAsync(new List { channel }, cancellationToken); - } - - /// - /// Subscribe the current installation to these channels. This is shorthand for: - /// - /// - /// var installation = ParseInstallation.CurrentInstallation; - /// installation.AddRangeUniqueToList("channels", channels); - /// installation.SaveAsync(); - /// - /// - /// The channels to which this installation should subscribe. - public static Task SubscribeAsync(IEnumerable channels) - { - return SubscribeAsync(channels, CancellationToken.None); - } - - /// - /// Subscribe the current installation to these channels. This is shorthand for: - /// - /// - /// var installation = ParseInstallation.CurrentInstallation; - /// installation.AddRangeUniqueToList("channels", channels); - /// installation.SaveAsync(cancellationToken); - /// - /// - /// The channels to which this installation should subscribe. - /// CancellationToken to cancel the current operation. - public static Task SubscribeAsync(IEnumerable channels, CancellationToken cancellationToken) - { - return PushChannelsController.SubscribeAsync(channels, cancellationToken); - } - - /// - /// Unsubscribe the current installation from this channel. This is shorthand for: - /// - /// - /// var installation = ParseInstallation.CurrentInstallation; - /// installation.Remove("channels", channel); - /// installation.SaveAsync(); - /// - /// - /// The channel from which this installation should unsubscribe. - public static Task UnsubscribeAsync(string channel) - { - return UnsubscribeAsync(new List { channel }, CancellationToken.None); - } - - /// - /// Unsubscribe the current installation from this channel. This is shorthand for: - /// - /// - /// var installation = ParseInstallation.CurrentInstallation; - /// installation.Remove("channels", channel); - /// installation.SaveAsync(cancellationToken); - /// - /// - /// The channel from which this installation should unsubscribe. - /// CancellationToken to cancel the current operation. - public static Task UnsubscribeAsync(string channel, CancellationToken cancellationToken) - { - return UnsubscribeAsync(new List { channel }, cancellationToken); - } - - /// - /// Unsubscribe the current installation from these channels. This is shorthand for: - /// - /// - /// var installation = ParseInstallation.CurrentInstallation; - /// installation.RemoveAllFromList("channels", channels); - /// installation.SaveAsync(); - /// - /// - /// The channels from which this installation should unsubscribe. - public static Task UnsubscribeAsync(IEnumerable channels) - { - return UnsubscribeAsync(channels, CancellationToken.None); - } - - /// - /// Unsubscribe the current installation from these channels. This is shorthand for: - /// - /// - /// var installation = ParseInstallation.CurrentInstallation; - /// installation.RemoveAllFromList("channels", channels); - /// installation.SaveAsync(cancellationToken); - /// - /// - /// The channels from which this installation should unsubscribe. - /// CancellationToken to cancel the current operation. - public static Task UnsubscribeAsync(IEnumerable channels, CancellationToken cancellationToken) - { - return PushChannelsController.UnsubscribeAsync(channels, cancellationToken); - } - - #endregion - } -} diff --git a/Parse/Public/ParseQueryExtensions.cs b/Parse/Public/ParseQueryExtensions.cs deleted file mode 100644 index 6b0d370b..00000000 --- a/Parse/Public/ParseQueryExtensions.cs +++ /dev/null @@ -1,823 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using Parse.Common.Internal; -using Parse.Core.Internal; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; - -namespace Parse -{ - /// - /// Provides extension methods for to support - /// Linq-style queries. - /// - public static class ParseQueryExtensions - { - private static readonly MethodInfo getMethod; - private static readonly MethodInfo stringContains; - private static readonly MethodInfo stringStartsWith; - private static readonly MethodInfo stringEndsWith; - private static readonly MethodInfo containsMethod; - private static readonly MethodInfo notContainsMethod; - private static readonly MethodInfo containsKeyMethod; - private static readonly MethodInfo notContainsKeyMethod; - private static readonly Dictionary functionMappings; - static ParseQueryExtensions() - { - getMethod = GetMethod(obj => obj.Get(null)).GetGenericMethodDefinition(); - stringContains = GetMethod(str => str.Contains(null)); - stringStartsWith = GetMethod(str => str.StartsWith(null)); - stringEndsWith = GetMethod(str => str.EndsWith(null)); - functionMappings = new Dictionary { - { - stringContains, - GetMethod>(q => q.WhereContains(null, null)) - }, - { - stringStartsWith, - GetMethod>(q => q.WhereStartsWith(null, null)) - }, - { - stringEndsWith, - GetMethod>(q => q.WhereEndsWith(null,null)) - }, - }; - containsMethod = GetMethod( - o => ParseQueryExtensions.ContainsStub(null, null)).GetGenericMethodDefinition(); - notContainsMethod = GetMethod( - o => ParseQueryExtensions.NotContainsStub(null, null)) - .GetGenericMethodDefinition(); - - containsKeyMethod = GetMethod(o => ParseQueryExtensions.ContainsKeyStub(null, null)); - notContainsKeyMethod = GetMethod( - o => ParseQueryExtensions.NotContainsKeyStub(null, null)); - } - - /// - /// Gets a MethodInfo for a top-level method call. - /// - private static MethodInfo GetMethod(Expression> expression) - { - return (expression.Body as MethodCallExpression).Method; - } - - /// - /// When a query is normalized, this is a placeholder to indicate we should - /// add a WhereContainedIn() clause. - /// - private static bool ContainsStub(object collection, T value) - { - throw new NotImplementedException( - "Exists only for expression translation as a placeholder."); - } - - /// - /// When a query is normalized, this is a placeholder to indicate we should - /// add a WhereNotContainedIn() clause. - /// - private static bool NotContainsStub(object collection, T value) - { - throw new NotImplementedException( - "Exists only for expression translation as a placeholder."); - } - - /// - /// When a query is normalized, this is a placeholder to indicate that we should - /// add a WhereExists() clause. - /// - private static bool ContainsKeyStub(ParseObject obj, string key) - { - throw new NotImplementedException( - "Exists only for expression translation as a placeholder."); - } - - /// - /// When a query is normalized, this is a placeholder to indicate that we should - /// add a WhereDoesNotExist() clause. - /// - private static bool NotContainsKeyStub(ParseObject obj, string key) - { - throw new NotImplementedException( - "Exists only for expression translation as a placeholder."); - } - - /// - /// Evaluates an expression and throws if the expression has components that can't be - /// evaluated (e.g. uses the parameter that's only represented by an object on the server). - /// - private static object GetValue(Expression exp) - { - try - { - return Expression.Lambda( - typeof(Func<>).MakeGenericType(exp.Type), exp).Compile().DynamicInvoke(); - } - catch (Exception e) - { - throw new InvalidOperationException("Unable to evaluate expression: " + exp, e); - } - } - - /// - /// Checks whether the MethodCallExpression is a call to ParseObject.Get(), - /// which is the call we normalize all indexing into the ParseObject to. - /// - private static bool IsParseObjectGet(MethodCallExpression node) - { - if (node == null || node.Object == null) - { - return false; - } - if (!typeof(ParseObject).GetTypeInfo().IsAssignableFrom(node.Object.Type.GetTypeInfo())) - { - return false; - } - return node.Method.IsGenericMethod && node.Method.GetGenericMethodDefinition() == getMethod; - } - - - /// - /// Visits an Expression, converting ParseObject.Get/ParseObject[]/ParseObject.Property, - /// and nested indices into a single call to ParseObject.Get() with a "field path" like - /// "foo.bar.baz" - /// - private class ObjectNormalizer : ExpressionVisitor - { - protected override Expression VisitIndex(IndexExpression node) - { - var visitedObject = Visit(node.Object); - var indexer = visitedObject as MethodCallExpression; - if (IsParseObjectGet(indexer)) - { - var indexValue = GetValue(node.Arguments[0]) as string; - if (indexValue == null) - { - throw new InvalidOperationException("Index must be a string"); - } - var newPath = GetValue(indexer.Arguments[0]) + "." + indexValue; - return Expression.Call(indexer.Object, - getMethod.MakeGenericMethod(node.Type), - Expression.Constant(newPath, typeof(string))); - } - return base.VisitIndex(node); - } - - /// - /// Check for a ParseFieldName attribute and use that as the path component, turning - /// properties like foo.ObjectId into foo.Get("objectId") - /// - protected override Expression VisitMember(MemberExpression node) - { - var fieldName = node.Member.GetCustomAttribute(); - if (fieldName != null && - typeof(ParseObject).GetTypeInfo().IsAssignableFrom(node.Expression.Type.GetTypeInfo())) - { - var newPath = fieldName.FieldName; - return Expression.Call(node.Expression, - getMethod.MakeGenericMethod(node.Type), - Expression.Constant(newPath, typeof(string))); - } - return base.VisitMember(node); - } - - /// - /// If a ParseObject.Get() call has been cast, just change the generic parameter. - /// - protected override Expression VisitUnary(UnaryExpression node) - { - var methodCall = Visit(node.Operand) as MethodCallExpression; - if ((node.NodeType == ExpressionType.Convert || - node.NodeType == ExpressionType.ConvertChecked) && - IsParseObjectGet(methodCall)) - { - return Expression.Call(methodCall.Object, - getMethod.MakeGenericMethod(node.Type), - methodCall.Arguments); - } - return base.VisitUnary(node); - } - - protected override Expression VisitMethodCall(MethodCallExpression node) - { - // Turn parseObject["foo"] into parseObject.Get("foo") - if (node.Method.Name == "get_Item" && node.Object is ParameterExpression) - { - var indexPath = GetValue(node.Arguments[0]) as string; - return Expression.Call(node.Object, - getMethod.MakeGenericMethod(typeof(object)), - Expression.Constant(indexPath, typeof(string))); - } - - // Turn parseObject.Get("foo")["bar"] into parseObject.Get("foo.bar") - if (node.Method.Name == "get_Item" || IsParseObjectGet(node)) - { - var visitedObject = Visit(node.Object); - var indexer = visitedObject as MethodCallExpression; - if (IsParseObjectGet(indexer)) - { - var indexValue = GetValue(node.Arguments[0]) as string; - if (indexValue == null) - { - throw new InvalidOperationException("Index must be a string"); - } - var newPath = GetValue(indexer.Arguments[0]) + "." + indexValue; - return Expression.Call(indexer.Object, - getMethod.MakeGenericMethod(node.Type), - Expression.Constant(newPath, typeof(string))); - } - } - return base.VisitMethodCall(node); - } - } - - /// - /// Normalizes Where expressions. - /// - private class WhereNormalizer : ExpressionVisitor - { - - /// - /// Normalizes binary operators. <, >, <=, >= !=, and == - /// This puts the ParseObject.Get() on the left side of the operation - /// (reversing it if necessary), and normalizes the ParseObject.Get() - /// - protected override Expression VisitBinary(BinaryExpression node) - { - var leftTransformed = new ObjectNormalizer().Visit(node.Left) as MethodCallExpression; - var rightTransformed = new ObjectNormalizer().Visit(node.Right) as MethodCallExpression; - - MethodCallExpression objectExpression; - Expression filterExpression; - bool inverted; - if (leftTransformed != null) - { - objectExpression = leftTransformed; - filterExpression = node.Right; - inverted = false; - } - else - { - objectExpression = rightTransformed; - filterExpression = node.Left; - inverted = true; - } - - try - { - switch (node.NodeType) - { - case ExpressionType.GreaterThan: - if (inverted) - { - return Expression.LessThan(objectExpression, filterExpression); - } - else - { - return Expression.GreaterThan(objectExpression, filterExpression); - } - case ExpressionType.GreaterThanOrEqual: - if (inverted) - { - return Expression.LessThanOrEqual(objectExpression, filterExpression); - } - else - { - return Expression.GreaterThanOrEqual(objectExpression, filterExpression); - } - case ExpressionType.LessThan: - if (inverted) - { - return Expression.GreaterThan(objectExpression, filterExpression); - } - else - { - return Expression.LessThan(objectExpression, filterExpression); - } - case ExpressionType.LessThanOrEqual: - if (inverted) - { - return Expression.GreaterThanOrEqual(objectExpression, filterExpression); - } - else - { - return Expression.LessThanOrEqual(objectExpression, filterExpression); - } - case ExpressionType.Equal: - return Expression.Equal(objectExpression, filterExpression); - case ExpressionType.NotEqual: - return Expression.NotEqual(objectExpression, filterExpression); - } - } - catch (ArgumentException) - { - throw new InvalidOperationException("Operation not supported: " + node); - } - return base.VisitBinary(node); - } - - /// - /// If a ! operator is used, this removes the ! and instead calls the equivalent - /// function (so e.g. == becomes !=, < becomes >=, Contains becomes NotContains) - /// - protected override Expression VisitUnary(UnaryExpression node) - { - // Normalizes inversion - if (node.NodeType == ExpressionType.Not) - { - var visitedOperand = Visit(node.Operand); - var binaryOperand = visitedOperand as BinaryExpression; - if (binaryOperand != null) - { - switch (binaryOperand.NodeType) - { - case ExpressionType.GreaterThan: - return Expression.LessThanOrEqual(binaryOperand.Left, binaryOperand.Right); - case ExpressionType.GreaterThanOrEqual: - return Expression.LessThan(binaryOperand.Left, binaryOperand.Right); - case ExpressionType.LessThan: - return Expression.GreaterThanOrEqual(binaryOperand.Left, binaryOperand.Right); - case ExpressionType.LessThanOrEqual: - return Expression.GreaterThan(binaryOperand.Left, binaryOperand.Right); - case ExpressionType.Equal: - return Expression.NotEqual(binaryOperand.Left, binaryOperand.Right); - case ExpressionType.NotEqual: - return Expression.Equal(binaryOperand.Left, binaryOperand.Right); - } - } - - var methodCallOperand = visitedOperand as MethodCallExpression; - if (methodCallOperand != null) - { - if (methodCallOperand.Method.IsGenericMethod) - { - if (methodCallOperand.Method.GetGenericMethodDefinition() == containsMethod) - { - var genericNotContains = notContainsMethod.MakeGenericMethod( - methodCallOperand.Method.GetGenericArguments()); - return Expression.Call(genericNotContains, methodCallOperand.Arguments.ToArray()); - } - if (methodCallOperand.Method.GetGenericMethodDefinition() == notContainsMethod) - { - var genericContains = containsMethod.MakeGenericMethod( - methodCallOperand.Method.GetGenericArguments()); - return Expression.Call(genericContains, methodCallOperand.Arguments.ToArray()); - } - } - if (methodCallOperand.Method == containsKeyMethod) - { - return Expression.Call(notContainsKeyMethod, methodCallOperand.Arguments.ToArray()); - } - if (methodCallOperand.Method == notContainsKeyMethod) - { - return Expression.Call(containsKeyMethod, methodCallOperand.Arguments.ToArray()); - } - } - } - return base.VisitUnary(node); - } - - /// - /// Normalizes .Equals into == and Contains() into the appropriate stub. - /// - protected override Expression VisitMethodCall(MethodCallExpression node) - { - // Convert .Equals() into == - if (node.Method.Name == "Equals" && - node.Method.ReturnType == typeof(bool) && - node.Method.GetParameters().Length == 1) - { - var obj = new ObjectNormalizer().Visit(node.Object) as MethodCallExpression; - var parameter = new ObjectNormalizer().Visit(node.Arguments[0]) as MethodCallExpression; - if ((IsParseObjectGet(obj) && (obj.Object is ParameterExpression)) || - (IsParseObjectGet(parameter) && (parameter.Object is ParameterExpression))) - { - return Expression.Equal(node.Object, node.Arguments[0]); - } - } - - // Convert the .Contains() into a ContainsStub - if (node.Method != stringContains && - node.Method.Name == "Contains" && - node.Method.ReturnType == typeof(bool) && - node.Method.GetParameters().Length <= 2) - { - var collection = node.Method.GetParameters().Length == 1 ? - node.Object : - node.Arguments[0]; - var parameterIndex = node.Method.GetParameters().Length - 1; - var parameter = new ObjectNormalizer().Visit(node.Arguments[parameterIndex]) - as MethodCallExpression; - if (IsParseObjectGet(parameter) && (parameter.Object is ParameterExpression)) - { - var genericContains = containsMethod.MakeGenericMethod(parameter.Type); - return Expression.Call(genericContains, collection, parameter); - } - var target = new ObjectNormalizer().Visit(collection) as MethodCallExpression; - var element = node.Arguments[parameterIndex]; - if (IsParseObjectGet(target) && (target.Object is ParameterExpression)) - { - var genericContains = containsMethod.MakeGenericMethod(element.Type); - return Expression.Call(genericContains, target, element); - } - } - - // Convert obj["foo.bar"].ContainsKey("baz") into obj.ContainsKey("foo.bar.baz") - if (node.Method.Name == "ContainsKey" && - node.Method.ReturnType == typeof(bool) && - node.Method.GetParameters().Length == 1) - { - var getter = new ObjectNormalizer().Visit(node.Object) as MethodCallExpression; - Expression target = null; - string path = null; - if (IsParseObjectGet(getter) && getter.Object is ParameterExpression) - { - target = getter.Object; - path = GetValue(getter.Arguments[0]) + "." + GetValue(node.Arguments[0]); - return Expression.Call(containsKeyMethod, target, Expression.Constant(path)); - } - else if (node.Object is ParameterExpression) - { - target = node.Object; - path = GetValue(node.Arguments[0]) as string; - } - if (target != null && path != null) - { - return Expression.Call(containsKeyMethod, target, Expression.Constant(path)); - } - } - return base.VisitMethodCall(node); - } - } - - /// - /// Converts a normalized method call expression into the appropriate ParseQuery clause. - /// - private static ParseQuery WhereMethodCall( - this ParseQuery source, Expression> expression, MethodCallExpression node) - where T : ParseObject - { - if (IsParseObjectGet(node) && (node.Type == typeof(bool) || node.Type == typeof(bool?))) - { - // This is a raw boolean field access like 'where obj.Get("foo")' - return source.WhereEqualTo(GetValue(node.Arguments[0]) as string, true); - } - - MethodInfo translatedMethod; - if (functionMappings.TryGetValue(node.Method, out translatedMethod)) - { - var objTransformed = new ObjectNormalizer().Visit(node.Object) as MethodCallExpression; - if (!(IsParseObjectGet(objTransformed) && - objTransformed.Object == expression.Parameters[0])) - { - throw new InvalidOperationException( - "The left-hand side of a supported function call must be a ParseObject field access."); - } - var fieldPath = GetValue(objTransformed.Arguments[0]); - var containedIn = GetValue(node.Arguments[0]); - var queryType = translatedMethod.DeclaringType.GetGenericTypeDefinition() - .MakeGenericType(typeof(T)); - translatedMethod = ReflectionHelpers.GetMethod(queryType, - translatedMethod.Name, - translatedMethod.GetParameters().Select(p => p.ParameterType).ToArray()); - return translatedMethod.Invoke(source, new[] { fieldPath, containedIn }) as ParseQuery; - } - - if (node.Arguments[0] == expression.Parameters[0]) - { - // obj.ContainsKey("foo") --> query.WhereExists("foo") - if (node.Method == containsKeyMethod) - { - return source.WhereExists(GetValue(node.Arguments[1]) as string); - } - // !obj.ContainsKey("foo") --> query.WhereDoesNotExist("foo") - if (node.Method == notContainsKeyMethod) - { - return source.WhereDoesNotExist(GetValue(node.Arguments[1]) as string); - } - } - - if (node.Method.IsGenericMethod) - { - if (node.Method.GetGenericMethodDefinition() == containsMethod) - { - // obj.Get>("path").Contains(someValue) - if (IsParseObjectGet(node.Arguments[0] as MethodCallExpression)) - { - return source.WhereEqualTo( - GetValue(((MethodCallExpression) node.Arguments[0]).Arguments[0]) as string, - GetValue(node.Arguments[1])); - } - // someList.Contains(obj.Get("path")) - if (IsParseObjectGet(node.Arguments[1] as MethodCallExpression)) - { - var collection = GetValue(node.Arguments[0]) as System.Collections.IEnumerable; - return source.WhereContainedIn( - GetValue(((MethodCallExpression) node.Arguments[1]).Arguments[0]) as string, - collection.Cast()); - } - } - - if (node.Method.GetGenericMethodDefinition() == notContainsMethod) - { - // !obj.Get>("path").Contains(someValue) - if (IsParseObjectGet(node.Arguments[0] as MethodCallExpression)) - { - return source.WhereNotEqualTo( - GetValue(((MethodCallExpression) node.Arguments[0]).Arguments[0]) as string, - GetValue(node.Arguments[1])); - } - // !someList.Contains(obj.Get("path")) - if (IsParseObjectGet(node.Arguments[1] as MethodCallExpression)) - { - var collection = GetValue(node.Arguments[0]) as System.Collections.IEnumerable; - return source.WhereNotContainedIn( - GetValue(((MethodCallExpression) node.Arguments[1]).Arguments[0]) as string, - collection.Cast()); - } - } - } - throw new InvalidOperationException(node.Method + " is not a supported method call in a where expression."); - } - - /// - /// Converts a normalized binary expression into the appropriate ParseQuery clause. - /// - private static ParseQuery WhereBinaryExpression( - this ParseQuery source, Expression> expression, BinaryExpression node) - where T : ParseObject - { - var leftTransformed = new ObjectNormalizer().Visit(node.Left) as MethodCallExpression; - - if (!(IsParseObjectGet(leftTransformed) && - leftTransformed.Object == expression.Parameters[0])) - { - throw new InvalidOperationException( - "Where expressions must have one side be a field operation on a ParseObject."); - } - - var fieldPath = GetValue(leftTransformed.Arguments[0]) as string; - var filterValue = GetValue(node.Right); - - if (filterValue != null && !ParseEncoder.IsValidType(filterValue)) - { - throw new InvalidOperationException( - "Where clauses must use types compatible with ParseObjects."); - } - - switch (node.NodeType) - { - case ExpressionType.GreaterThan: - return source.WhereGreaterThan(fieldPath, filterValue); - case ExpressionType.GreaterThanOrEqual: - return source.WhereGreaterThanOrEqualTo(fieldPath, filterValue); - case ExpressionType.LessThan: - return source.WhereLessThan(fieldPath, filterValue); - case ExpressionType.LessThanOrEqual: - return source.WhereLessThanOrEqualTo(fieldPath, filterValue); - case ExpressionType.Equal: - return source.WhereEqualTo(fieldPath, filterValue); - case ExpressionType.NotEqual: - return source.WhereNotEqualTo(fieldPath, filterValue); - default: - throw new InvalidOperationException( - "Where expressions do not support this operator."); - } - } - - /// - /// Filters a query based upon the predicate provided. - /// - /// The type of ParseObject being queried for. - /// The base to which - /// the predicate will be added. - /// A function to test each ParseObject for a condition. - /// The predicate must be able to be represented by one of the standard Where - /// functions on ParseQuery - /// A new ParseQuery whose results will match the given predicate as - /// well as the source's filters. - public static ParseQuery Where( - this ParseQuery source, Expression> predicate) - where TSource : ParseObject - { - // Handle top-level logic operators && and || - var binaryExpression = predicate.Body as BinaryExpression; - if (binaryExpression != null) - { - if (binaryExpression.NodeType == ExpressionType.AndAlso) - { - return source - .Where(Expression.Lambda>( - binaryExpression.Left, predicate.Parameters)) - .Where(Expression.Lambda>( - binaryExpression.Right, predicate.Parameters)); - } - if (binaryExpression.NodeType == ExpressionType.OrElse) - { - var left = source.Where(Expression.Lambda>( - binaryExpression.Left, predicate.Parameters)); - var right = source.Where(Expression.Lambda>( - binaryExpression.Right, predicate.Parameters)); - return left.Or(right); - } - } - - var normalized = new WhereNormalizer().Visit(predicate.Body); - - var methodCallExpr = normalized as MethodCallExpression; - if (methodCallExpr != null) - { - return source.WhereMethodCall(predicate, methodCallExpr); - } - - var binaryExpr = normalized as BinaryExpression; - if (binaryExpr != null) - { - return source.WhereBinaryExpression(predicate, binaryExpr); - } - - var unaryExpr = normalized as UnaryExpression; - if (unaryExpr != null && unaryExpr.NodeType == ExpressionType.Not) - { - var node = unaryExpr.Operand as MethodCallExpression; - if (IsParseObjectGet(node) && (node.Type == typeof(bool) || node.Type == typeof(bool?))) - { - // This is a raw boolean field access like 'where !obj.Get("foo")' - return source.WhereNotEqualTo(GetValue(node.Arguments[0]) as string, true); - } - } - - throw new InvalidOperationException( - "Encountered an unsupported expression for ParseQueries."); - } - - /// - /// Normalizes an OrderBy's keySelector expression and then extracts the path - /// from the ParseObject.Get() call. - /// - private static string GetOrderByPath( - Expression> keySelector) - { - string result = null; - var normalized = new ObjectNormalizer().Visit(keySelector.Body); - var callExpr = normalized as MethodCallExpression; - if (IsParseObjectGet(callExpr) && callExpr.Object == keySelector.Parameters[0]) - { - // We're operating on the parameter - result = GetValue(callExpr.Arguments[0]) as string; - } - if (result == null) - { - throw new InvalidOperationException( - "OrderBy expression must be a field access on a ParseObject."); - } - return result; - } - - /// - /// Orders a query based upon the key selector provided. - /// - /// The type of ParseObject being queried for. - /// The type of key returned by keySelector. - /// The query to order. - /// A function to extract a key from the ParseObject. - /// A new ParseQuery based on source whose results will be ordered by - /// the key specified in the keySelector. - public static ParseQuery OrderBy( - this ParseQuery source, Expression> keySelector) - where TSource : ParseObject - { - return source.OrderBy(GetOrderByPath(keySelector)); - } - - /// - /// Orders a query based upon the key selector provided. - /// - /// The type of ParseObject being queried for. - /// The type of key returned by keySelector. - /// The query to order. - /// A function to extract a key from the ParseObject. - /// A new ParseQuery based on source whose results will be ordered by - /// the key specified in the keySelector. - public static ParseQuery OrderByDescending( - this ParseQuery source, Expression> keySelector) - where TSource : ParseObject - { - return source.OrderByDescending(GetOrderByPath(keySelector)); - } - - /// - /// Performs a subsequent ordering of a query based upon the key selector provided. - /// - /// The type of ParseObject being queried for. - /// The type of key returned by keySelector. - /// The query to order. - /// A function to extract a key from the ParseObject. - /// A new ParseQuery based on source whose results will be ordered by - /// the key specified in the keySelector. - public static ParseQuery ThenBy( - this ParseQuery source, Expression> keySelector) - where TSource : ParseObject - { - return source.ThenBy(GetOrderByPath(keySelector)); - } - - /// - /// Performs a subsequent ordering of a query based upon the key selector provided. - /// - /// The type of ParseObject being queried for. - /// The type of key returned by keySelector. - /// The query to order. - /// A function to extract a key from the ParseObject. - /// A new ParseQuery based on source whose results will be ordered by - /// the key specified in the keySelector. - public static ParseQuery ThenByDescending( - this ParseQuery source, Expression> keySelector) - where TSource : ParseObject - { - return source.ThenByDescending(GetOrderByPath(keySelector)); - } - - /// - /// Correlates the elements of two queries based on matching keys. - /// - /// The type of ParseObjects of the first query. - /// The type of ParseObjects of the second query. - /// The type of the keys returned by the key selector - /// functions. - /// The type of the result. This must match either - /// TOuter or TInner - /// The first query to join. - /// The query to join to the first query. - /// A function to extract a join key from the results of - /// the first query. - /// A function to extract a join key from the results of - /// the second query. - /// A function to select either the outer or inner query - /// result to determine which query is the base query. - /// A new ParseQuery with a WhereMatchesQuery or WhereMatchesKeyInQuery - /// clause based upon the query indicated in the . - public static ParseQuery Join( - this ParseQuery outer, - ParseQuery inner, - Expression> outerKeySelector, - Expression> innerKeySelector, - Expression> resultSelector) - where TOuter : ParseObject - where TInner : ParseObject - where TResult : ParseObject - { - // resultSelector must select either the inner object or the outer object. If it's the inner - // object, reverse the query. - if (resultSelector.Body == resultSelector.Parameters[1]) - { - // The inner object was selected. - return inner.Join( - outer, - innerKeySelector, - outerKeySelector, - (i, o) => i) as ParseQuery; - } - if (resultSelector.Body != resultSelector.Parameters[0]) - { - throw new InvalidOperationException("Joins must select either the outer or inner object."); - } - - // Normalize both selectors - Expression outerNormalized = new ObjectNormalizer().Visit(outerKeySelector.Body); - Expression innerNormalized = new ObjectNormalizer().Visit(innerKeySelector.Body); - MethodCallExpression outerAsGet = outerNormalized as MethodCallExpression; - MethodCallExpression innerAsGet = innerNormalized as MethodCallExpression; - if (IsParseObjectGet(outerAsGet) && outerAsGet.Object == outerKeySelector.Parameters[0]) - { - var outerKey = GetValue(outerAsGet.Arguments[0]) as string; - - if (IsParseObjectGet(innerAsGet) && innerAsGet.Object == innerKeySelector.Parameters[0]) - { - // Both are key accesses, so treat this as a WhereMatchesKeyInQuery - var innerKey = GetValue(innerAsGet.Arguments[0]) as string; - return outer.WhereMatchesKeyInQuery(outerKey, innerKey, inner) as ParseQuery; - } - - if (innerKeySelector.Body == innerKeySelector.Parameters[0]) - { - // The inner selector is on the result of the query itself, so treat this as a - // WhereMatchesQuery - return outer.WhereMatchesQuery(outerKey, inner) as ParseQuery; - } - throw new InvalidOperationException( - "The key for the joined object must be a ParseObject or a field access " + - "on the ParseObject."); - } - - // TODO (hallucinogen): If we ever support "and" queries fully and/or support a "where this object - // matches some key in some other query" (as opposed to requiring a key on this query), we - // can add support for even more types of joins. - - throw new InvalidOperationException( - "The key for the selected object must be a field access on the ParseObject."); - } - } -} diff --git a/Parse/Public/ParseRelation.cs b/Parse/Public/ParseRelation.cs deleted file mode 100644 index 0f8b6da8..00000000 --- a/Parse/Public/ParseRelation.cs +++ /dev/null @@ -1,168 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using Parse.Core.Internal; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Text; -using Parse.Common.Internal; - -namespace Parse -{ - /// - /// A common base class for ParseRelations. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public abstract class ParseRelationBase : IJsonConvertible - { - private ParseObject parent; - private string key; - private string targetClassName; - - internal ParseRelationBase(ParseObject parent, string key) - { - EnsureParentAndKey(parent, key); - } - - internal ParseRelationBase(ParseObject parent, string key, string targetClassName) - : this(parent, key) - { - this.targetClassName = targetClassName; - } - - internal static IObjectSubclassingController SubclassingController - { - get - { - return ParseCorePlugins.Instance.SubclassingController; - } - } - - internal void EnsureParentAndKey(ParseObject parent, string key) - { - this.parent = this.parent ?? parent; - this.key = this.key ?? key; - Debug.Assert(this.parent == parent, "Relation retrieved from two different objects"); - Debug.Assert(this.key == key, "Relation retrieved from two different keys"); - } - - internal void Add(ParseObject obj) - { - var change = new ParseRelationOperation(new[] { obj }, null); - parent.PerformOperation(key, change); - targetClassName = change.TargetClassName; - } - - internal void Remove(ParseObject obj) - { - var change = new ParseRelationOperation(null, new[] { obj }); - parent.PerformOperation(key, change); - targetClassName = change.TargetClassName; - } - - IDictionary IJsonConvertible.ToJSON() - { - return new Dictionary { - {"__type", "Relation"}, - {"className", targetClassName} - }; - } - - internal ParseQuery GetQuery() where T : ParseObject - { - if (targetClassName != null) - { - return new ParseQuery(targetClassName) - .WhereRelatedTo(parent, key); - } - - return new ParseQuery(parent.ClassName) - .RedirectClassName(key) - .WhereRelatedTo(parent, key); - } - - internal string TargetClassName - { - get - { - return targetClassName; - } - set - { - targetClassName = value; - } - } - - /// - /// Produces the proper ParseRelation<T> instance for the given classname. - /// - internal static ParseRelationBase CreateRelation(ParseObject parent, - string key, - string targetClassName) - { - var targetType = SubclassingController.GetType(targetClassName) ?? typeof(ParseObject); - - Expression>> createRelationExpr = - () => CreateRelation(parent, key, targetClassName); - var createRelationMethod = - ((MethodCallExpression) createRelationExpr.Body) - .Method - .GetGenericMethodDefinition() - .MakeGenericMethod(targetType); - return (ParseRelationBase) createRelationMethod.Invoke(null, new object[] { parent, key, targetClassName }); - } - - private static ParseRelation CreateRelation(ParseObject parent, string key, string targetClassName) - where T : ParseObject - { - return new ParseRelation(parent, key, targetClassName); - } - } - - /// - /// Provides access to all of the children of a many-to-many relationship. Each instance of - /// ParseRelation is associated with a particular parent and key. - /// - /// The type of the child objects. - public sealed class ParseRelation : ParseRelationBase where T : ParseObject - { - - internal ParseRelation(ParseObject parent, string key) : base(parent, key) { } - - internal ParseRelation(ParseObject parent, string key, string targetClassName) - : base(parent, key, targetClassName) { } - - /// - /// Adds an object to this relation. The object must already have been saved. - /// - /// The object to add. - public void Add(T obj) - { - base.Add(obj); - } - - /// - /// Removes an object from this relation. The object must already have been saved. - /// - /// The object to remove. - public void Remove(T obj) - { - base.Remove(obj); - } - - /// - /// Gets a query that can be used to query the objects in this relation. - /// - public ParseQuery Query - { - get - { - return base.GetQuery(); - } - } - } -} diff --git a/Parse/Public/ParseSession.cs b/Parse/Public/ParseSession.cs deleted file mode 100644 index 6f438c7c..00000000 --- a/Parse/Public/ParseSession.cs +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using Parse.Core.Internal; -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Parse.Common.Internal; - -namespace Parse -{ - /// - /// Represents a session of a user for a Parse application. - /// - [ParseClassName("_Session")] - public class ParseSession : ParseObject - { - private static readonly HashSet readOnlyKeys = new HashSet { - "sessionToken", "createdWith", "restricted", "user", "expiresAt", "installationId" - }; - - protected override bool IsKeyMutable(string key) - { - return !readOnlyKeys.Contains(key); - } - - /// - /// Gets the session token for a user, if they are logged in. - /// - [ParseFieldName("sessionToken")] - public string SessionToken - { - get { return GetProperty(null, "SessionToken"); } - } - - /// - /// Constructs a for ParseSession. - /// - public static ParseQuery Query - { - get - { - return new ParseQuery(); - } - } - - internal static IParseSessionController SessionController - { - get - { - return ParseCorePlugins.Instance.SessionController; - } - } - - /// - /// Gets the current object related to the current user. - /// - public static Task GetCurrentSessionAsync() - { - return GetCurrentSessionAsync(CancellationToken.None); - } - - /// - /// Gets the current object related to the current user. - /// - /// The cancellation token - public static Task GetCurrentSessionAsync(CancellationToken cancellationToken) - { - return ParseUser.GetCurrentUserAsync().OnSuccess(t1 => - { - ParseUser user = t1.Result; - if (user == null) - { - return Task.FromResult((ParseSession) null); - } - - string sessionToken = user.SessionToken; - if (sessionToken == null) - { - return Task.FromResult((ParseSession) null); - } - - return SessionController.GetSessionAsync(sessionToken, cancellationToken).OnSuccess(t => - { - ParseSession session = ParseObject.FromState(t.Result, "_Session"); - return session; - }); - }).Unwrap(); - } - - internal static Task RevokeAsync(string sessionToken, CancellationToken cancellationToken) - { - if (sessionToken == null || !SessionController.IsRevocableSessionToken(sessionToken)) - { - return Task.FromResult(0); - } - return SessionController.RevokeAsync(sessionToken, cancellationToken); - } - - internal static Task UpgradeToRevocableSessionAsync(string sessionToken, CancellationToken cancellationToken) - { - if (sessionToken == null || SessionController.IsRevocableSessionToken(sessionToken)) - { - return Task.FromResult(sessionToken); - } - - return SessionController.UpgradeToRevocableSessionAsync(sessionToken, cancellationToken).OnSuccess(t => - { - ParseSession session = ParseObject.FromState(t.Result, "_Session"); - return session.SessionToken; - }); - } - } -} diff --git a/Parse/Public/ParseUser.cs b/Parse/Public/ParseUser.cs deleted file mode 100644 index 78c8318f..00000000 --- a/Parse/Public/ParseUser.cs +++ /dev/null @@ -1,600 +0,0 @@ -// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. - -using Parse.Core.Internal; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Parse.Common.Internal; - -namespace Parse -{ - /// - /// Represents a user for a Parse application. - /// - [ParseClassName("_User")] - public class ParseUser : ParseObject - { - private static readonly IDictionary authProviders = new Dictionary(); - - private static readonly HashSet readOnlyKeys = new HashSet { "sessionToken", "isNew" }; - - internal static IParseUserController UserController => ParseCorePlugins.Instance.UserController; - - internal static IParseCurrentUserController CurrentUserController => ParseCorePlugins.Instance.CurrentUserController; - - /// - /// Whether the ParseUser has been authenticated on this device. Only an authenticated - /// ParseUser can be saved and deleted. - /// - public bool IsAuthenticated - { - get - { - lock (mutex) - { - return SessionToken != null && CurrentUser != null && CurrentUser.ObjectId == ObjectId; - } - } - } - - /// - /// Removes a key from the object's data if it exists. - /// - /// The key to remove. - /// Cannot remove the username key. - public override void Remove(string key) - { - if (key == "username") - { - throw new ArgumentException("Cannot remove the username key."); - } - base.Remove(key); - } - - protected override bool IsKeyMutable(string key) => !readOnlyKeys.Contains(key); - - internal override void HandleSave(IObjectState serverState) - { - base.HandleSave(serverState); - - SynchronizeAllAuthData(); - CleanupAuthData(); - - MutateState(mutableClone => mutableClone.ServerData.Remove("password")); - } - - public string SessionToken => State.ContainsKey("sessionToken") ? State["sessionToken"] as string : null; - - internal static string CurrentSessionToken - { - get - { - Task sessionTokenTask = GetCurrentSessionTokenAsync(); - sessionTokenTask.Wait(); - return sessionTokenTask.Result; - } - } - - internal static Task GetCurrentSessionTokenAsync(CancellationToken cancellationToken = default) => CurrentUserController.GetCurrentSessionTokenAsync(cancellationToken); - - internal Task SetSessionTokenAsync(string newSessionToken) => SetSessionTokenAsync(newSessionToken, CancellationToken.None); - - internal Task SetSessionTokenAsync(string newSessionToken, CancellationToken cancellationToken) - { - MutateState(mutableClone => mutableClone.ServerData["sessionToken"] = newSessionToken); - - return SaveCurrentUserAsync(this); - } - - /// - /// Gets or sets the username. - /// - [ParseFieldName("username")] - public string Username - { - get => GetProperty(null, "Username"); - set => SetProperty(value, "Username"); - } - - /// - /// Sets the password. - /// - [ParseFieldName("password")] - public string Password - { - private get => GetProperty(null, "Password"); - set => SetProperty(value, "Password"); - } - - /// - /// Sets the email address. - /// - [ParseFieldName("email")] - public string Email - { - get => GetProperty(null, "Email"); - set => SetProperty(value, "Email"); - } - - internal Task SignUpAsync(Task toAwait, CancellationToken cancellationToken) - { - if (AuthData == null) - { - // TODO (hallucinogen): make an Extension of Task to create Task with exception/canceled. - if (String.IsNullOrEmpty(Username)) - { - TaskCompletionSource tcs = new TaskCompletionSource(); - tcs.TrySetException(new InvalidOperationException("Cannot sign up user with an empty name.")); - return tcs.Task; - } - if (String.IsNullOrEmpty(Password)) - { - TaskCompletionSource tcs = new TaskCompletionSource(); - tcs.TrySetException(new InvalidOperationException("Cannot sign up user with an empty password.")); - return tcs.Task; - } - } - if (!String.IsNullOrEmpty(ObjectId)) - { - TaskCompletionSource tcs = new TaskCompletionSource(); - tcs.TrySetException(new InvalidOperationException("Cannot sign up a user that already exists.")); - return tcs.Task; - } - - IDictionary currentOperations = StartSave(); - - return toAwait.OnSuccess(_ => UserController.SignUpAsync(State, currentOperations, cancellationToken)).Unwrap().ContinueWith(t => - { - if (t.IsFaulted || t.IsCanceled) - { - HandleFailedSave(currentOperations); - } - else - { - HandleSave(t.Result); - } - return t; - }).Unwrap().OnSuccess(_ => SaveCurrentUserAsync(this)).Unwrap(); - } - - /// - /// Signs up a new user. This will create a new ParseUser on the server and will also persist the - /// session on disk so that you can access the user using . A username and - /// password must be set before calling SignUpAsync. - /// - public Task SignUpAsync() => SignUpAsync(CancellationToken.None); - - /// - /// Signs up a new user. This will create a new ParseUser on the server and will also persist the - /// session on disk so that you can access the user using . A username and - /// password must be set before calling SignUpAsync. - /// - /// The cancellation token. - public Task SignUpAsync(CancellationToken cancellationToken) => taskQueue.Enqueue(toAwait => SignUpAsync(toAwait, cancellationToken), cancellationToken); - - /// - /// Logs in a user with a username and password. On success, this saves the session to disk so you - /// can retrieve the currently logged in user using . - /// - /// The username to log in with. - /// The password to log in with. - /// The newly logged-in user. - public static Task LogInAsync(string username, string password) => LogInAsync(username, password, CancellationToken.None); - - /// - /// Logs in a user with a username and password. On success, this saves the session to disk so you - /// can retrieve the currently logged in user using . - /// - /// The username to log in with. - /// The password to log in with. - /// The cancellation token. - /// The newly logged-in user. - public static Task LogInAsync(string username, string password, CancellationToken cancellationToken) => UserController.LogInAsync(username, password, cancellationToken).OnSuccess(t => - { - ParseUser user = FromState(t.Result, "_User"); - return SaveCurrentUserAsync(user).OnSuccess(_ => user); - }).Unwrap(); - - /// - /// Logs in a user with a username and password. On success, this saves the session to disk so you - /// can retrieve the currently logged in user using . - /// - /// The session token to authorize with - /// The user if authorization was successful - public static Task BecomeAsync(string sessionToken) => BecomeAsync(sessionToken, CancellationToken.None); - - /// - /// Logs in a user with a username and password. On success, this saves the session to disk so you - /// can retrieve the currently logged in user using . - /// - /// The session token to authorize with - /// The cancellation token. - /// The user if authorization was successful - public static Task BecomeAsync(string sessionToken, CancellationToken cancellationToken) => UserController.GetUserAsync(sessionToken, cancellationToken).OnSuccess(t => - { - ParseUser user = FromState(t.Result, "_User"); - return SaveCurrentUserAsync(user).OnSuccess(_ => user); - }).Unwrap(); - - protected override Task SaveAsync(Task toAwait, CancellationToken cancellationToken) - { - lock (mutex) - { - if (ObjectId == null) - { - throw new InvalidOperationException("You must call SignUpAsync before calling SaveAsync."); - } - return base.SaveAsync(toAwait, cancellationToken).OnSuccess(_ => - { - if (!CurrentUserController.IsCurrent(this)) - { - return Task.FromResult(0); - } - return SaveCurrentUserAsync(this); - }).Unwrap(); - } - } - - // If this is already the current user, refresh its state on disk. - internal override Task FetchAsyncInternal(Task toAwait, CancellationToken cancellationToken) => base.FetchAsyncInternal(toAwait, cancellationToken).OnSuccess(t => !CurrentUserController.IsCurrent(this) ? Task.FromResult(t.Result) : SaveCurrentUserAsync(this).OnSuccess(_ => t.Result)).Unwrap(); - - /// - /// Logs out the currently logged in user session. This will remove the session from disk, log out of - /// linked services, and future calls to will return null. - /// - /// - /// Typically, you should use , unless you are managing your own threading. - /// - public static void LogOut() => LogOutAsync().Wait(); // TODO (hallucinogen): this will without a doubt fail in Unity. But what else can we do? - - /// - /// Logs out the currently logged in user session. This will remove the session from disk, log out of - /// linked services, and future calls to will return null. - /// - /// - /// This is preferable to using , unless your code is already running from a - /// background thread. - /// - public static Task LogOutAsync() => LogOutAsync(CancellationToken.None); - - /// - /// Logs out the currently logged in user session. This will remove the session from disk, log out of - /// linked services, and future calls to will return null. - /// - /// This is preferable to using , unless your code is already running from a - /// background thread. - /// - public static Task LogOutAsync(CancellationToken cancellationToken) - { - return GetCurrentUserAsync().OnSuccess(t => - { - LogOutWithProviders(); - - ParseUser user = t.Result; - if (user == null) - { - return Task.FromResult(0); - } - - return user.taskQueue.Enqueue(toAwait => user.LogOutAsync(toAwait, cancellationToken), cancellationToken); - }).Unwrap(); - } - - internal Task LogOutAsync(Task toAwait, CancellationToken cancellationToken) - { - string oldSessionToken = SessionToken; - if (oldSessionToken == null) - { - return Task.FromResult(0); - } - - // Cleanup in-memory session. - MutateState(mutableClone => - { - mutableClone.ServerData.Remove("sessionToken"); - }); - var revokeSessionTask = ParseSession.RevokeAsync(oldSessionToken, cancellationToken); - return Task.WhenAll(revokeSessionTask, CurrentUserController.LogOutAsync(cancellationToken)); - } - - private static void LogOutWithProviders() - { - foreach (var provider in authProviders.Values) - { - provider.Deauthenticate(); - } - } - - /// - /// Gets the currently logged in ParseUser with a valid session, either from memory or disk - /// if necessary. - /// - public static ParseUser CurrentUser - { - get - { - var userTask = GetCurrentUserAsync(); - // TODO (hallucinogen): this will without a doubt fail in Unity. How should we fix it? - userTask.Wait(); - return userTask.Result; - } - } - - /// - /// Gets the currently logged in ParseUser with a valid session, either from memory or disk - /// if necessary, asynchronously. - /// - internal static Task GetCurrentUserAsync() => GetCurrentUserAsync(CancellationToken.None); - - /// - /// Gets the currently logged in ParseUser with a valid session, either from memory or disk - /// if necessary, asynchronously. - /// - internal static Task GetCurrentUserAsync(CancellationToken cancellationToken) => CurrentUserController.GetAsync(cancellationToken); - - private static Task SaveCurrentUserAsync(ParseUser user) => SaveCurrentUserAsync(user, CancellationToken.None); - - private static Task SaveCurrentUserAsync(ParseUser user, CancellationToken cancellationToken) => CurrentUserController.SetAsync(user, cancellationToken); - - internal static void ClearInMemoryUser() => CurrentUserController.ClearFromMemory(); - - /// - /// Constructs a for ParseUsers. - /// - public static ParseQuery Query => new ParseQuery(); - - #region Legacy / Revocable Session Tokens - - private static readonly object isRevocableSessionEnabledMutex = new object(); - private static bool isRevocableSessionEnabled; - - /// - /// Tells server to use revocable session on LogIn and SignUp, even when App's Settings - /// has "Require Revocable Session" turned off. Issues network request in background to - /// migrate the sessionToken on disk to revocable session. - /// - /// The Task that upgrades the session. - public static Task EnableRevocableSessionAsync() - { - return EnableRevocableSessionAsync(CancellationToken.None); - } - - /// - /// Tells server to use revocable session on LogIn and SignUp, even when App's Settings - /// has "Require Revocable Session" turned off. Issues network request in background to - /// migrate the sessionToken on disk to revocable session. - /// - /// The Task that upgrades the session. - public static Task EnableRevocableSessionAsync(CancellationToken cancellationToken) - { - lock (isRevocableSessionEnabledMutex) - { - isRevocableSessionEnabled = true; - } - - return GetCurrentUserAsync(cancellationToken).OnSuccess(t => - { - var user = t.Result; - return user.UpgradeToRevocableSessionAsync(cancellationToken); - }); - } - - internal static void DisableRevocableSession() - { - lock (isRevocableSessionEnabledMutex) - { - isRevocableSessionEnabled = false; - } - } - - internal static bool IsRevocableSessionEnabled - { - get - { - lock (isRevocableSessionEnabledMutex) - { - return isRevocableSessionEnabled; - } - } - } - - internal Task UpgradeToRevocableSessionAsync() - { - return UpgradeToRevocableSessionAsync(CancellationToken.None); - } - - internal Task UpgradeToRevocableSessionAsync(CancellationToken cancellationToken) - { - return taskQueue.Enqueue(toAwait => UpgradeToRevocableSessionAsync(toAwait, cancellationToken), - cancellationToken); - } - - internal Task UpgradeToRevocableSessionAsync(Task toAwait, CancellationToken cancellationToken) - { - string sessionToken = SessionToken; - - return toAwait.OnSuccess(_ => - { - return ParseSession.UpgradeToRevocableSessionAsync(sessionToken, cancellationToken); - }).Unwrap().OnSuccess(t => - { - return SetSessionTokenAsync(t.Result); - }).Unwrap(); - } - - #endregion - - /// - /// Requests a password reset email to be sent to the specified email address associated with the - /// user account. This email allows the user to securely reset their password on the Parse site. - /// - /// The email address associated with the user that forgot their password. - public static Task RequestPasswordResetAsync(string email) => RequestPasswordResetAsync(email, CancellationToken.None); - - /// - /// Requests a password reset email to be sent to the specified email address associated with the - /// user account. This email allows the user to securely reset their password on the Parse site. - /// - /// The email address associated with the user that forgot their password. - /// The cancellation token. - public static Task RequestPasswordResetAsync(string email, CancellationToken cancellationToken) => UserController.RequestPasswordResetAsync(email, cancellationToken); - - /// - /// Gets the authData for this user. - /// - internal IDictionary> AuthData - { - get => TryGetValue("authData", out IDictionary> authData) ? authData : null; - private set => this["authData"] = value; - } - - private static IParseAuthenticationProvider GetProvider(string providerName) => authProviders.TryGetValue(providerName, out IParseAuthenticationProvider provider) ? provider : null; - - /// - /// Removes null values from authData (which exist temporarily for unlinking) - /// - private void CleanupAuthData() - { - lock (mutex) - { - if (!CurrentUserController.IsCurrent(this)) - { - return; - } - IDictionary> authData = AuthData; - - if (authData == null) - { - return; - } - - foreach (KeyValuePair> pair in new Dictionary>(authData)) - { - if (pair.Value == null) - { - authData.Remove(pair.Key); - } - } - } - } - - /// - /// Synchronizes authData for all providers. - /// - private void SynchronizeAllAuthData() - { - lock (mutex) - { - IDictionary> authData = AuthData; - - if (authData == null) - { - return; - } - - foreach (var pair in authData) - { - SynchronizeAuthData(GetProvider(pair.Key)); - } - } - } - - private void SynchronizeAuthData(IParseAuthenticationProvider provider) - { - bool restorationSuccess = false; - lock (mutex) - { - IDictionary> authData = AuthData; - if (authData == null || provider == null) - { - return; - } - if (authData.TryGetValue(provider.AuthType, out IDictionary data)) - { - restorationSuccess = provider.RestoreAuthentication(data); - } - } - - if (!restorationSuccess) - { - UnlinkFromAsync(provider.AuthType, CancellationToken.None); - } - } - - internal Task LinkWithAsync(string authType, IDictionary data, CancellationToken cancellationToken) => taskQueue.Enqueue(toAwait => - { - IDictionary> authData = AuthData; - if (authData == null) - { - authData = AuthData = new Dictionary>(); - } - authData[authType] = data; - AuthData = authData; - return SaveAsync(cancellationToken); - }, cancellationToken); - - internal Task LinkWithAsync(string authType, CancellationToken cancellationToken) - { - IParseAuthenticationProvider provider = GetProvider(authType); - return provider.AuthenticateAsync(cancellationToken).OnSuccess(t => LinkWithAsync(authType, t.Result, cancellationToken)).Unwrap(); - } - - /// - /// Unlinks a user from a service. - /// - internal Task UnlinkFromAsync(string authType, CancellationToken cancellationToken) => LinkWithAsync(authType, null, cancellationToken); - - /// - /// Checks whether a user is linked to a service. - /// - internal bool IsLinked(string authType) - { - lock (mutex) - { - return AuthData != null && AuthData.ContainsKey(authType) && AuthData[authType] != null; - } - } - - internal static Task LogInWithAsync(string authType, IDictionary data, CancellationToken cancellationToken) - { - ParseUser user = null; - - return UserController.LogInAsync(authType, data, cancellationToken).OnSuccess(t => - { - user = FromState(t.Result, "_User"); - - lock (user.mutex) - { - if (user.AuthData == null) - { - user.AuthData = new Dictionary>(); - } - user.AuthData[authType] = data; - user.SynchronizeAllAuthData(); - } - - return SaveCurrentUserAsync(user); - }).Unwrap().OnSuccess(t => user); - } - - internal static Task LogInWithAsync(string authType, CancellationToken cancellationToken) - { - IParseAuthenticationProvider provider = GetProvider(authType); - return provider.AuthenticateAsync(cancellationToken).OnSuccess(authData => LogInWithAsync(authType, authData.Result, cancellationToken)).Unwrap(); - } - - internal static void RegisterProvider(IParseAuthenticationProvider provider) - { - authProviders[provider.AuthType] = provider; - ParseUser curUser = CurrentUser; - if (curUser != null) - { - curUser.SynchronizeAuthData(provider); - } - } - } -} diff --git a/Parse/Resources.Designer.cs b/Parse/Resources.Designer.cs new file mode 100644 index 00000000..3b6e39ff --- /dev/null +++ b/Parse/Resources.Designer.cs @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Parse { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Parse.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Mutation of a file-backed cache is an asynchronous operation as the tracked file needs to be modified.. + /// + internal static string FileBackedCacheSynchronousMutationNotSupportedMessage { + get { + return ResourceManager.GetString("FileBackedCacheSynchronousMutationNotSupportedMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot perform file operations with in-memory cache controller.. + /// + internal static string TransientCacheControllerDiskFileOperationNotSupportedMessage { + get { + return ResourceManager.GetString("TransientCacheControllerDiskFileOperationNotSupportedMessage", resourceCulture); + } + } + } +} diff --git a/Parse/Resources.resx b/Parse/Resources.resx new file mode 100644 index 00000000..69ace474 --- /dev/null +++ b/Parse/Resources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Mutation of a file-backed cache is an asynchronous operation as the tracked file needs to be modified. + + + Cannot perform file operations with in-memory cache controller. + + \ No newline at end of file diff --git a/Parse/Public/ParseAnalytics.cs b/Parse/Utilities/AnalyticsServiceExtensions.cs similarity index 67% rename from Parse/Public/ParseAnalytics.cs rename to Parse/Utilities/AnalyticsServiceExtensions.cs index 9c08cf39..1e9e4d3b 100644 --- a/Parse/Public/ParseAnalytics.cs +++ b/Parse/Utilities/AnalyticsServiceExtensions.cs @@ -3,10 +3,8 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using System.Threading; -using Parse.Common.Internal; -using Parse.Analytics.Internal; -using Parse.Core.Internal; +using Parse.Abstractions.Infrastructure; +using Parse.Infrastructure.Utilities; namespace Parse { @@ -16,32 +14,13 @@ namespace Parse /// Methods will return immediately and cache requests (along with timestamps) /// to be handled in the background. /// - public partial class ParseAnalytics + public static class AnalyticsServiceExtensions { - internal static IParseAnalyticsController AnalyticsController - { - get - { - return ParseAnalyticsPlugins.Instance.AnalyticsController; - } - } - - internal static IParseCurrentUserController CurrentUserController - { - get - { - return ParseAnalyticsPlugins.Instance.CorePlugins.CurrentUserController; - } - } - /// /// Tracks this application being launched. /// /// An Async Task that can be waited on or ignored. - public static Task TrackAppOpenedAsync() - { - return ParseAnalytics.TrackAppOpenedWithPushHashAsync(); - } + public static Task TrackLaunchAsync(this IServiceHub serviceHub) => TrackLaunchWithPushHashAsync(serviceHub); /// /// Tracks the occurrence of a custom event with additional dimensions. @@ -67,10 +46,7 @@ public static Task TrackAppOpenedAsync() /// The name of the custom event to report to ParseClient /// as having happened. /// An Async Task that can be waited on or ignored. - public static Task TrackEventAsync(string name) - { - return TrackEventAsync(name, null); - } + public static Task TrackAnalyticsEventAsync(this IServiceHub serviceHub, string name) => TrackAnalyticsEventAsync(serviceHub, name, default); /// /// Tracks the occurrence of a custom event with additional dimensions. @@ -98,21 +74,14 @@ public static Task TrackEventAsync(string name) /// The dictionary of information by which to /// segment this event. /// An Async Task that can be waited on or ignored. - public static Task TrackEventAsync(string name, IDictionary dimensions) + public static Task TrackAnalyticsEventAsync(this IServiceHub serviceHub, string name, IDictionary dimensions) { - if (name == null || name.Trim().Length == 0) + if (name is null || name.Trim().Length == 0) { throw new ArgumentException("A name for the custom event must be provided."); } - return CurrentUserController.GetCurrentSessionTokenAsync(CancellationToken.None) - .OnSuccess(t => - { - return AnalyticsController.TrackEventAsync(name, - dimensions, - t.Result, - CancellationToken.None); - }).Unwrap(); + return serviceHub.CurrentUserController.GetCurrentSessionTokenAsync(serviceHub).OnSuccess(task => serviceHub.AnalyticsController.TrackEventAsync(name, dimensions, task.Result, serviceHub)).Unwrap(); } /// @@ -122,15 +91,6 @@ public static Task TrackEventAsync(string name, IDictionary dime /// An identifying hash for a given push notification, /// passed down from the server. /// An Async Task that can be waited on or ignored. - private static Task TrackAppOpenedWithPushHashAsync(string pushHash = null) - { - return CurrentUserController.GetCurrentSessionTokenAsync(CancellationToken.None) - .OnSuccess(t => - { - return AnalyticsController.TrackAppOpenedAsync(pushHash, - t.Result, - CancellationToken.None); - }).Unwrap(); - } + static Task TrackLaunchWithPushHashAsync(this IServiceHub serviceHub, string pushHash = null) => serviceHub.CurrentUserController.GetCurrentSessionTokenAsync(serviceHub).OnSuccess(task => serviceHub.AnalyticsController.TrackAppOpenedAsync(pushHash, task.Result, serviceHub)).Unwrap(); } } diff --git a/Parse/Public/ParseCloud.cs b/Parse/Utilities/CloudCodeServiceExtensions.cs similarity index 73% rename from Parse/Public/ParseCloud.cs rename to Parse/Utilities/CloudCodeServiceExtensions.cs index 7c533a2b..cfcde189 100644 --- a/Parse/Public/ParseCloud.cs +++ b/Parse/Utilities/CloudCodeServiceExtensions.cs @@ -1,10 +1,9 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. -using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Parse.Core.Internal; +using Parse.Abstractions.Infrastructure; namespace Parse { @@ -21,16 +20,8 @@ namespace Parse /// await ParseCloud.CallFunctionAsync<IDictionary<string, object>>("validateGame", parameters); /// /// - public static class ParseCloud + public static class CloudCodeServiceExtensions { - internal static IParseCloudCodeController CloudCodeController - { - get - { - return ParseCorePlugins.Instance.CloudCodeController; - } - } - /// /// Calls a cloud function. /// @@ -42,10 +33,7 @@ internal static IParseCloudCodeController CloudCodeController /// dictionary can contain anything that could be passed into a ParseObject except for /// ParseObjects themselves. /// The result of the cloud call. - public static Task CallFunctionAsync(string name, IDictionary parameters) - { - return CallFunctionAsync(name, parameters, CancellationToken.None); - } + public static Task CallCloudCodeFunctionAsync(this IServiceHub serviceHub, string name, IDictionary parameters) => CallCloudCodeFunctionAsync(serviceHub, name, parameters, CancellationToken.None); /// /// Calls a cloud function. @@ -59,13 +47,6 @@ public static Task CallFunctionAsync(string name, IDictionary /// The cancellation token. /// The result of the cloud call. - public static Task CallFunctionAsync(string name, - IDictionary parameters, CancellationToken cancellationToken) - { - return CloudCodeController.CallFunctionAsync(name, - parameters, - ParseUser.CurrentSessionToken, - cancellationToken); - } + public static Task CallCloudCodeFunctionAsync(this IServiceHub serviceHub, string name, IDictionary parameters, CancellationToken cancellationToken) => serviceHub.CloudCodeController.CallFunctionAsync(name, parameters, serviceHub.GetCurrentSessionToken(), serviceHub, cancellationToken); } } diff --git a/Parse/Utilities/ConfigurationServiceExtensions.cs b/Parse/Utilities/ConfigurationServiceExtensions.cs new file mode 100644 index 00000000..019b3299 --- /dev/null +++ b/Parse/Utilities/ConfigurationServiceExtensions.cs @@ -0,0 +1,43 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Infrastructure.Data; +using Parse.Platform.Configuration; + +namespace Parse +{ + public static class ConfigurationServiceExtensions + { + public static ParseConfiguration BuildConfiguration(this IServiceHub serviceHub, IDictionary configurationData) => ParseConfiguration.Create(configurationData, serviceHub.Decoder, serviceHub); + + public static ParseConfiguration BuildConfiguration(this IParseDataDecoder dataDecoder, IDictionary configurationData, IServiceHub serviceHub) => ParseConfiguration.Create(configurationData, dataDecoder, serviceHub); + +#warning Investigate if these methods which simply block a thread waiting for an asynchronous process to complete should be eliminated. + + /// + /// Gets the latest fetched ParseConfig. + /// + /// ParseConfig object + public static ParseConfiguration GetCurrentConfiguration(this IServiceHub serviceHub) + { + Task task = serviceHub.ConfigurationController.CurrentConfigurationController.GetCurrentConfigAsync(serviceHub); + + task.Wait(); + return task.Result; + } + + internal static void ClearCurrentConfig(this IServiceHub serviceHub) => serviceHub.ConfigurationController.CurrentConfigurationController.ClearCurrentConfigAsync().Wait(); + + internal static void ClearCurrentConfigInMemory(this IServiceHub serviceHub) => serviceHub.ConfigurationController.CurrentConfigurationController.ClearCurrentConfigInMemoryAsync().Wait(); + + /// + /// Retrieves the ParseConfig asynchronously from the server. + /// + /// The cancellation token. + /// ParseConfig object that was fetched + public static Task GetConfigurationAsync(this IServiceHub serviceHub, CancellationToken cancellationToken = default) => serviceHub.ConfigurationController.FetchConfigAsync(serviceHub.GetCurrentSessionToken(), serviceHub, cancellationToken); + } +} diff --git a/Parse/Utilities/InstallationServiceExtensions.cs b/Parse/Utilities/InstallationServiceExtensions.cs new file mode 100644 index 00000000..afd4b9ad --- /dev/null +++ b/Parse/Utilities/InstallationServiceExtensions.cs @@ -0,0 +1,43 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure; + +namespace Parse +{ + public static class InstallationServiceExtensions + { + /// + /// Constructs a for ParseInstallations. + /// + /// + /// Only the following types of queries are allowed for installations: + /// + /// + /// query.GetAsync(objectId) + /// query.WhereEqualTo(key, value) + /// query.WhereMatchesKeyInQuery<TOther>(key, keyInQuery, otherQuery) + /// + /// + /// You can add additional query conditions, but one of the above must appear as a top-level AND + /// clause in the query. + /// + public static ParseQuery GetInstallationQuery(this IServiceHub serviceHub) => new ParseQuery(serviceHub); + +#warning Consider making the following method asynchronous. + + /// + /// Gets the ParseInstallation representing this app on this device. + /// + public static ParseInstallation GetCurrentInstallation(this IServiceHub serviceHub) + { + Task task = serviceHub.CurrentInstallationController.GetAsync(serviceHub); + + // TODO (hallucinogen): this will absolutely break on Unity, but how should we resolve this? + task.Wait(); + return task.Result; + } + + internal static void ClearInMemoryInstallation(this IServiceHub serviceHub) => serviceHub.CurrentInstallationController.ClearFromMemory(); + } +} diff --git a/Parse/Utilities/ObjectServiceExtensions.cs b/Parse/Utilities/ObjectServiceExtensions.cs new file mode 100644 index 00000000..2c7c2765 --- /dev/null +++ b/Parse/Utilities/ObjectServiceExtensions.cs @@ -0,0 +1,514 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Internal; +using Parse.Abstractions.Infrastructure.Control; +using Parse.Abstractions.Platform.Objects; +using Parse.Infrastructure.Utilities; +using Parse.Infrastructure.Data; + +namespace Parse +{ + public static class ObjectServiceExtensions + { + /// + /// Registers a custom subclass type with the Parse SDK, enabling strong-typing of those ParseObjects whenever + /// they appear. Subclasses must specify the ParseClassName attribute, have a default constructor, and properties + /// backed by ParseObject fields should have ParseFieldName attributes supplied. + /// + /// The target instance. + /// The ParseObject subclass type to register. + public static void AddValidClass(this IServiceHub serviceHub) where T : ParseObject, new() => serviceHub.ClassController.AddValid(typeof(T)); + + /// + /// Registers a custom subclass type with the Parse SDK, enabling strong-typing of those ParseObjects whenever + /// they appear. Subclasses must specify the ParseClassName attribute, have a default constructor, and properties + /// backed by ParseObject fields should have ParseFieldName attributes supplied. + /// + /// The ParseObject subclass type to register. + /// The target instance. + public static void RegisterSubclass(this IServiceHub serviceHub, Type type) + { + if (typeof(ParseObject).IsAssignableFrom(type)) + { + serviceHub.ClassController.AddValid(type); + } + } + + /// + /// Unregisters a previously-registered sub-class of with the subclassing controller. + /// + /// + /// + public static void RemoveClass(this IServiceHub serviceHub) where T : ParseObject, new() => serviceHub.ClassController.RemoveClass(typeof(T)); + + /// + /// Unregisters a previously-registered sub-class of with the subclassing controller. + /// + /// + /// + public static void RemoveClass(this IParseObjectClassController subclassingController, Type type) + { + if (typeof(ParseObject).IsAssignableFrom(type)) + { + subclassingController.RemoveClass(type); + } + } + + /// + /// Creates a new ParseObject based upon a class name. If the class name is a special type (e.g. + /// for ), then the appropriate type of ParseObject is returned. + /// + /// The class of object to create. + /// A new ParseObject for the given class name. + public static ParseObject CreateObject(this IServiceHub serviceHub, string className) => serviceHub.ClassController.Instantiate(className, serviceHub); + + /// + /// Creates a new ParseObject based upon a given subclass type. + /// + /// A new ParseObject for the given class name. + public static T CreateObject(this IServiceHub serviceHub) where T : ParseObject => (T) serviceHub.ClassController.CreateObject(serviceHub); + + /// + /// Creates a new ParseObject based upon a given subclass type. + /// + /// A new ParseObject for the given class name. + public static T CreateObject(this IParseObjectClassController classController, IServiceHub serviceHub) where T : ParseObject => (T) classController.Instantiate(classController.GetClassName(typeof(T)), serviceHub); + + /// + /// Creates a reference to an existing ParseObject for use in creating associations between + /// ParseObjects. Calling on this object will return + /// false until has been called. + /// No network request will be made. + /// + /// The object's class. + /// The object id for the referenced object. + /// A ParseObject without data. + public static ParseObject CreateObjectWithoutData(this IServiceHub serviceHub, string className, string objectId) => serviceHub.ClassController.CreateObjectWithoutData(className, objectId, serviceHub); + + /// + /// Creates a reference to an existing ParseObject for use in creating associations between + /// ParseObjects. Calling on this object will return + /// false until has been called. + /// No network request will be made. + /// + /// The object's class. + /// The object id for the referenced object. + /// A ParseObject without data. + public static ParseObject CreateObjectWithoutData(this IParseObjectClassController classController, string className, string objectId, IServiceHub serviceHub) + { + ParseObject.CreatingPointer.Value = true; + try + { + ParseObject result = classController.Instantiate(className, serviceHub); + result.ObjectId = objectId; + + // Left in because the property setter might be doing something funky. + + result.IsDirty = false; + return result.IsDirty ? throw new InvalidOperationException("A ParseObject subclass default constructor must not make changes to the object that cause it to be dirty.") : result; + } + finally { ParseObject.CreatingPointer.Value = false; } + } + + /// + /// Creates a reference to an existing ParseObject for use in creating associations between + /// ParseObjects. Calling on this object will return + /// false until has been called. + /// No network request will be made. + /// + /// The object id for the referenced object. + /// A ParseObject without data. + public static T CreateObjectWithoutData(this IServiceHub serviceHub, string objectId) where T : ParseObject => (T) serviceHub.CreateObjectWithoutData(serviceHub.ClassController.GetClassName(typeof(T)), objectId); + + /// + /// Deletes each object in the provided list. + /// + /// The objects to delete. + public static Task DeleteObjectsAsync(this IServiceHub serviceHub, IEnumerable objects) where T : ParseObject => DeleteObjectsAsync(serviceHub, objects, CancellationToken.None); + + /// + /// Deletes each object in the provided list. + /// + /// The objects to delete. + /// The cancellation token. + public static Task DeleteObjectsAsync(this IServiceHub serviceHub, IEnumerable objects, CancellationToken cancellationToken) where T : ParseObject + { + HashSet unique = new HashSet(objects.OfType().ToList(), new IdentityEqualityComparer { }); + + return EnqueueForAll(unique, toAwait => toAwait.OnSuccess(_ => Task.WhenAll(serviceHub.ObjectController.DeleteAllAsync(unique.Select(task => task.State).ToList(), serviceHub.GetCurrentSessionToken(), cancellationToken))).Unwrap().OnSuccess(task => + { + // Dirty all objects in memory. + + foreach (ParseObject obj in unique) + { + obj.IsDirty = true; + } + + return default(object); + }), cancellationToken); + } + + /// + /// Fetches all of the objects in the provided list. + /// + /// The objects to fetch. + /// The list passed in for convenience. + public static Task> FetchObjectsAsync(this IServiceHub serviceHub, IEnumerable objects) where T : ParseObject => FetchObjectsAsync(serviceHub, objects, CancellationToken.None); + + /// + /// Fetches all of the objects in the provided list. + /// + /// The objects to fetch. + /// The cancellation token. + /// The list passed in for convenience. + public static Task> FetchObjectsAsync(this IServiceHub serviceHub, IEnumerable objects, CancellationToken cancellationToken) where T : ParseObject => EnqueueForAll(objects.Cast(), (Task toAwait) => serviceHub.FetchAllInternalAsync(objects, true, toAwait, cancellationToken), cancellationToken); + + /// + /// Fetches all of the objects that don't have data in the provided list. + /// + /// todo: describe objects parameter on FetchAllIfNeededAsync + /// The list passed in for convenience. + public static Task> FetchObjectsIfNeededAsync(this IServiceHub serviceHub, IEnumerable objects) where T : ParseObject => FetchObjectsIfNeededAsync(serviceHub, objects, CancellationToken.None); + + /// + /// Fetches all of the objects that don't have data in the provided list. + /// + /// The objects to fetch. + /// The cancellation token. + /// The list passed in for convenience. + public static Task> FetchObjectsIfNeededAsync(this IServiceHub serviceHub, IEnumerable objects, CancellationToken cancellationToken) where T : ParseObject => EnqueueForAll(objects.Cast(), (Task toAwait) => serviceHub.FetchAllInternalAsync(objects, false, toAwait, cancellationToken), cancellationToken); + + /// + /// Gets a for the type of object specified by + /// + /// + /// The class name of the object. + /// A new . + public static ParseQuery GetQuery(this IServiceHub serviceHub, string className) + { + // Since we can't return a ParseQuery (due to strong-typing with + // generics), we'll require you to go through subclasses. This is a better + // experience anyway, especially with LINQ integration, since you'll get + // strongly-typed queries and compile-time checking of property names and + // types. + + if (serviceHub.ClassController.GetType(className) is { }) + { + throw new ArgumentException($"Use the class-specific query properties for class {className}", nameof(className)); + } + return new ParseQuery(serviceHub, className); + } + + /// + /// Saves each object in the provided list. + /// + /// The objects to save. + public static Task SaveObjectsAsync(this IServiceHub serviceHub, IEnumerable objects) where T : ParseObject => SaveObjectsAsync(serviceHub, objects, CancellationToken.None); + + /// + /// Saves each object in the provided list. + /// + /// The objects to save. + /// The cancellation token. + public static Task SaveObjectsAsync(this IServiceHub serviceHub, IEnumerable objects, CancellationToken cancellationToken) where T : ParseObject => DeepSaveAsync(serviceHub, objects.ToList(), serviceHub.GetCurrentSessionToken(), cancellationToken); + + /// + /// Flattens dictionaries and lists into a single enumerable of all contained objects + /// that can then be queried over. + /// + /// The root of the traversal + /// Whether to traverse into ParseObjects' children + /// Whether to include the root in the result + /// + internal static IEnumerable TraverseObjectDeep(this IServiceHub serviceHub, object root, bool traverseParseObjects = false, bool yieldRoot = false) + { + IEnumerable items = DeepTraversalInternal(serviceHub, root, traverseParseObjects, new HashSet(new IdentityEqualityComparer())); + return yieldRoot ? new[] { root }.Concat(items) : items; + } + + // TODO (hallucinogen): add unit test + internal static T GenerateObjectFromState(this IServiceHub serviceHub, IObjectState state, string defaultClassName) where T : ParseObject => serviceHub.ClassController.GenerateObjectFromState(state, defaultClassName, serviceHub); + + internal static T GenerateObjectFromState(this IParseObjectClassController classController, IObjectState state, string defaultClassName, IServiceHub serviceHub) where T : ParseObject + { + T obj = (T) classController.CreateObjectWithoutData(state.ClassName ?? defaultClassName, state.ObjectId, serviceHub); + obj.HandleFetchResult(state); + + return obj; + } + + internal static IDictionary GenerateJSONObjectForSaving(this IServiceHub serviceHub, IDictionary operations) + { + Dictionary result = new Dictionary(); + + foreach (KeyValuePair pair in operations) + { + result[pair.Key] = PointerOrLocalIdEncoder.Instance.Encode(pair.Value, serviceHub); + } + + return result; + } + + /// + /// Returns true if the given object can be serialized for saving as a value + /// that is pointed to by a ParseObject. + /// + internal static bool CanBeSerializedAsValue(this IServiceHub serviceHub, object value) => TraverseObjectDeep(serviceHub, value, yieldRoot: true).OfType().All(entity => entity.ObjectId is { }); + + static void CollectDirtyChildren(this IServiceHub serviceHub, object node, IList dirtyChildren, ICollection seen, ICollection seenNew) + { + foreach (ParseObject target in TraverseObjectDeep(serviceHub, node).OfType()) + { + ICollection scopedSeenNew; + + // Check for cycles of new objects. Any such cycle means it will be impossible to save + // this collection of objects, so throw an exception. + + if (target.ObjectId != null) + { + scopedSeenNew = new HashSet(new IdentityEqualityComparer()); + } + else + { + if (seenNew.Contains(target)) + { + throw new InvalidOperationException("Found a circular dependency while saving"); + } + + scopedSeenNew = new HashSet(seenNew, new IdentityEqualityComparer()) { target }; + } + + // Check for cycles of any object. If this occurs, then there's no problem, but + // we shouldn't recurse any deeper, because it would be an infinite recursion. + + if (seen.Contains(target)) + { + return; + } + + seen.Add(target); + + // Recurse into this object's children looking for dirty children. + // We only need to look at the child object's current estimated data, + // because that's the only data that might need to be saved now. + + CollectDirtyChildren(serviceHub, target.EstimatedData, dirtyChildren, seen, scopedSeenNew); + + if (target.CheckIsDirty(false)) + { + dirtyChildren.Add(target); + } + } + } + + /// + /// Helper version of CollectDirtyChildren so that callers don't have to add the internally + /// used parameters. + /// + static void CollectDirtyChildren(this IServiceHub serviceHub, object node, IList dirtyChildren) => CollectDirtyChildren(serviceHub, node, dirtyChildren, new HashSet(new IdentityEqualityComparer()), new HashSet(new IdentityEqualityComparer())); + + internal static Task DeepSaveAsync(this IServiceHub serviceHub, object target, string sessionToken, CancellationToken cancellationToken) + { + List objects = new List(); + CollectDirtyChildren(serviceHub, target, objects); + + HashSet uniqueObjects = new HashSet(objects, new IdentityEqualityComparer()); + List saveDirtyFileTasks = TraverseObjectDeep(serviceHub, target, true).OfType().Where(file => file.IsDirty).Select(file => file.SaveAsync(serviceHub, cancellationToken)).ToList(); + + return Task.WhenAll(saveDirtyFileTasks).OnSuccess(_ => + { + IEnumerable remaining = new List(uniqueObjects); + return InternalExtensions.WhileAsync(() => Task.FromResult(remaining.Any()), () => + { + // Partition the objects into two sets: those that can be saved immediately, + // and those that rely on other objects to be created first. + + List current = (from item in remaining where item.CanBeSerialized select item).ToList(), nextBatch = (from item in remaining where !item.CanBeSerialized select item).ToList(); + remaining = nextBatch; + + if (current.Count == 0) + { + // We do cycle-detection when building the list of objects passed to this + // function, so this should never get called. But we should check for it + // anyway, so that we get an exception instead of an infinite loop. + + throw new InvalidOperationException("Unable to save a ParseObject with a relation to a cycle."); + } + + // Save all of the objects in current. + + return EnqueueForAll(current, toAwait => toAwait.OnSuccess(__ => + { + List states = (from item in current select item.State).ToList(); + List> operationsList = (from item in current select item.StartSave()).ToList(); + + IList> saveTasks = serviceHub.ObjectController.SaveAllAsync(states, operationsList, sessionToken, serviceHub, cancellationToken); + + return Task.WhenAll(saveTasks).ContinueWith(task => + { + if (task.IsFaulted || task.IsCanceled) + { + foreach ((ParseObject item, IDictionary ops) pair in current.Zip(operationsList, (item, ops) => (item, ops))) + { + pair.item.HandleFailedSave(pair.ops); + } + } + else + { + foreach ((ParseObject item, IObjectState state) pair in current.Zip(task.Result, (item, state) => (item, state))) + { + pair.item.HandleSave(pair.state); + } + } + + cancellationToken.ThrowIfCancellationRequested(); + return task; + }).Unwrap(); + }).Unwrap().OnSuccess(t => (object) null), cancellationToken); + }); + }).Unwrap(); + } + + static IEnumerable DeepTraversalInternal(this IServiceHub serviceHub, object root, bool traverseParseObjects, ICollection seen) + { + seen.Add(root); + System.Collections.IEnumerable targets = ParseClient.IL2CPPCompiled ? null : null as IEnumerable; + + if (Conversion.As>(root) is { } rootDictionary) + { + targets = rootDictionary.Values; + } + else + { + if (Conversion.As>(root) is { } rootList) + { + targets = rootList; + } + else if (traverseParseObjects) + { + if (root is ParseObject entity) + { + targets = entity.Keys.ToList().Select(key => entity[key]); + } + } + } + + if (targets is { }) + { + foreach (object item in targets) + { + if (!seen.Contains(item)) + { + yield return item; + + foreach (object child in DeepTraversalInternal(serviceHub, item, traverseParseObjects, seen)) + { + yield return child; + } + } + } + } + } + + /// + /// Adds a task to the queue for all of the given objects. + /// + static Task EnqueueForAll(IEnumerable objects, Func> taskStart, CancellationToken cancellationToken) + { + // The task that will be complete when all of the child queues indicate they're ready to start. + + TaskCompletionSource readyToStart = new TaskCompletionSource(); + + // First, we need to lock the mutex for the queue for every object. We have to hold this + // from at least when taskStart() is called to when obj.taskQueue enqueue is called, so + // that saves actually get executed in the order they were setup by taskStart(). + // The locks have to be sorted so that we always acquire them in the same order. + // Otherwise, there's some risk of deadlock. + + LockSet lockSet = new LockSet(objects.Select(o => o.TaskQueue.Mutex)); + + lockSet.Enter(); + try + { + // The task produced by taskStart. By running this immediately, we allow everything prior + // to toAwait to run before waiting for all of the queues on all of the objects. + + Task fullTask = taskStart(readyToStart.Task); + + // Add fullTask to each of the objects' queues. + + List childTasks = new List(); + foreach (ParseObject obj in objects) + { + obj.TaskQueue.Enqueue((Task task) => + { + childTasks.Add(task); + return fullTask; + }, cancellationToken); + } + + // When all of the objects' queues are ready, signal fullTask that it's ready to go on. + Task.WhenAll(childTasks.ToArray()).ContinueWith((Task task) => readyToStart.SetResult(default)); + return fullTask; + } + finally + { + lockSet.Exit(); + } + } + + /// + /// Fetches all of the objects in the list. + /// + /// The objects to fetch. + /// If false, only objects without data will be fetched. + /// A task to await before starting. + /// The cancellation token. + /// The list passed in for convenience. + static Task> FetchAllInternalAsync(this IServiceHub serviceHub, IEnumerable objects, bool force, Task toAwait, CancellationToken cancellationToken) where T : ParseObject => toAwait.OnSuccess(_ => + { + if (objects.Any(obj => obj.State.ObjectId == null)) + { + throw new InvalidOperationException("You cannot fetch objects that haven't already been saved."); + } + + List objectsToFetch = (from obj in objects where force || !obj.IsDataAvailable select obj).ToList(); + + if (objectsToFetch.Count == 0) + { + return Task.FromResult(objects); + } + + // Do one Find for each class. + + Dictionary>> findsByClass = (from obj in objectsToFetch group obj.ObjectId by obj.ClassName into classGroup where classGroup.Count() > 0 select (ClassName: classGroup.Key, FindTask: new ParseQuery(serviceHub, classGroup.Key).WhereContainedIn("objectId", classGroup).FindAsync(cancellationToken))).ToDictionary(pair => pair.ClassName, pair => pair.FindTask); + + // Wait for all the Finds to complete. + + return Task.WhenAll(findsByClass.Values.ToList()).OnSuccess(__ => + { + if (cancellationToken.IsCancellationRequested) + { + return objects; + } + + // Merge the data from the Finds into the input objects. + foreach ((T obj, ParseObject result) in from obj in objectsToFetch from result in findsByClass[obj.ClassName].Result where result.ObjectId == obj.ObjectId select (obj, result)) + { + obj.MergeFromObject(result); + obj.Fetched = true; + } + + return objects; + }); + }).Unwrap(); + + internal static string GetFieldForPropertyName(this IServiceHub serviceHub, string className, string propertyName) => serviceHub.ClassController.GetPropertyMappings(className).TryGetValue(propertyName, out string fieldName) ? fieldName : fieldName; + } +} diff --git a/Parse/Utilities/ParseExtensions.cs b/Parse/Utilities/ParseExtensions.cs new file mode 100644 index 00000000..682a1234 --- /dev/null +++ b/Parse/Utilities/ParseExtensions.cs @@ -0,0 +1,42 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System.Threading; +using System.Threading.Tasks; +using Parse.Infrastructure.Utilities; + +namespace Parse +{ + /// + /// Provides convenience extension methods for working with collections + /// of ParseObjects so that you can easily save and fetch them in batches. + /// + public static class ParseExtensions + { + /// + /// Fetches this object with the data from the server. + /// + public static Task FetchAsync(this T obj) where T : ParseObject => obj.FetchAsyncInternal(CancellationToken.None).OnSuccess(t => (T) t.Result); + + /// + /// Fetches this object with the data from the server. + /// + /// The ParseObject to fetch. + /// The cancellation token. + public static Task FetchAsync(this T target, CancellationToken cancellationToken) where T : ParseObject => target.FetchAsyncInternal(cancellationToken).OnSuccess(task => (T) task.Result); + + /// + /// If this ParseObject has not been fetched (i.e. returns + /// false), fetches this object with the data from the server. + /// + /// The ParseObject to fetch. + public static Task FetchIfNeededAsync(this T obj) where T : ParseObject => obj.FetchIfNeededAsyncInternal(CancellationToken.None).OnSuccess(t => (T) t.Result); + + /// + /// If this ParseObject has not been fetched (i.e. returns + /// false), fetches this object with the data from the server. + /// + /// The ParseObject to fetch. + /// The cancellation token. + public static Task FetchIfNeededAsync(this T obj, CancellationToken cancellationToken) where T : ParseObject => obj.FetchIfNeededAsyncInternal(cancellationToken).OnSuccess(t => (T) t.Result); + } +} diff --git a/Parse/Internal/Utilities/ParseFileExtensions.cs b/Parse/Utilities/ParseFileExtensions.cs similarity index 85% rename from Parse/Internal/Utilities/ParseFileExtensions.cs rename to Parse/Utilities/ParseFileExtensions.cs index a5bbd236..1f5f3e2e 100644 --- a/Parse/Internal/Utilities/ParseFileExtensions.cs +++ b/Parse/Utilities/ParseFileExtensions.cs @@ -1,9 +1,8 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. using System; -using System.Collections.Generic; -namespace Parse.Core.Internal +namespace Parse.Abstractions.Internal { /// /// So here's the deal. We have a lot of internal APIs for ParseObject, ParseUser, etc. @@ -17,9 +16,6 @@ namespace Parse.Core.Internal /// public static class ParseFileExtensions { - public static ParseFile Create(string name, Uri uri, string mimeType = null) - { - return new ParseFile(name, uri, mimeType); - } + public static ParseFile Create(string name, Uri uri, string mimeType = null) => new ParseFile(name, uri, mimeType); } } diff --git a/Parse/Utilities/ParseQueryExtensions.cs b/Parse/Utilities/ParseQueryExtensions.cs new file mode 100644 index 00000000..b8e2d816 --- /dev/null +++ b/Parse/Utilities/ParseQueryExtensions.cs @@ -0,0 +1,686 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Parse.Infrastructure.Data; + +namespace Parse.Abstractions.Internal +{ +#warning Fully refactor at some point. + + /// + /// So here's the deal. We have a lot of internal APIs for ParseObject, ParseUser, etc. + /// + /// These cannot be 'internal' anymore if we are fully modularizing things out, because + /// they are no longer a part of the same library, especially as we create things like + /// Installation inside push library. + /// + /// So this class contains a bunch of extension methods that can live inside another + /// namespace, which 'wrap' the intenral APIs that already exist. + /// + public static class ParseQueryExtensions + { + static MethodInfo ParseObjectGetMethod { get; } + + static MethodInfo StringContainsMethod { get; } + + static MethodInfo StringStartsWithMethod { get; } + + static MethodInfo StringEndsWithMethod { get; } + + static MethodInfo ContainsMethod { get; } + + static MethodInfo NotContainsMethod { get; } + + static MethodInfo ContainsKeyMethod { get; } + + static MethodInfo NotContainsKeyMethod { get; } + + static Dictionary Mappings { get; } + + static ParseQueryExtensions() + { + ParseObjectGetMethod = GetMethod(target => target.Get(null)).GetGenericMethodDefinition(); + StringContainsMethod = GetMethod(text => text.Contains(null)); + StringStartsWithMethod = GetMethod(text => text.StartsWith(null)); + StringEndsWithMethod = GetMethod(text => text.EndsWith(null)); + + Mappings = new Dictionary + { + [StringContainsMethod] = GetMethod>(query => query.WhereContains(null, null)), + [StringStartsWithMethod] = GetMethod>(query => query.WhereStartsWith(null, null)), + [StringEndsWithMethod] = GetMethod>(query => query.WhereEndsWith(null, null)), + }; + + ContainsMethod = GetMethod(o => ContainsStub(null, null)).GetGenericMethodDefinition(); + NotContainsMethod = GetMethod(o => NotContainsStub(null, null)).GetGenericMethodDefinition(); + + ContainsKeyMethod = GetMethod(o => ContainsKeyStub(null, null)); + NotContainsKeyMethod = GetMethod(o => NotContainsKeyStub(null, null)); + } + + /// + /// Gets a MethodInfo for a top-level method call. + /// + static MethodInfo GetMethod(Expression> expression) => (expression.Body as MethodCallExpression).Method; + + /// + /// When a query is normalized, this is a placeholder to indicate we should + /// add a WhereContainedIn() clause. + /// + static bool ContainsStub(object collection, T value) => throw new NotImplementedException("Exists only for expression translation as a placeholder."); + + /// + /// When a query is normalized, this is a placeholder to indicate we should + /// add a WhereNotContainedIn() clause. + /// + static bool NotContainsStub(object collection, T value) => throw new NotImplementedException("Exists only for expression translation as a placeholder."); + + /// + /// When a query is normalized, this is a placeholder to indicate that we should + /// add a WhereExists() clause. + /// + static bool ContainsKeyStub(ParseObject obj, string key) => throw new NotImplementedException("Exists only for expression translation as a placeholder."); + + /// + /// When a query is normalized, this is a placeholder to indicate that we should + /// add a WhereDoesNotExist() clause. + /// + static bool NotContainsKeyStub(ParseObject obj, string key) => throw new NotImplementedException("Exists only for expression translation as a placeholder."); + + /// + /// Evaluates an expression and throws if the expression has components that can't be + /// evaluated (e.g. uses the parameter that's only represented by an object on the server). + /// + static object GetValue(Expression exp) + { + try + { + return Expression.Lambda(typeof(Func<>).MakeGenericType(exp.Type), exp).Compile().DynamicInvoke(); + } + catch (Exception e) + { + throw new InvalidOperationException("Unable to evaluate expression: " + exp, e); + } + } + + /// + /// Checks whether the MethodCallExpression is a call to ParseObject.Get(), + /// which is the call we normalize all indexing into the ParseObject to. + /// + static bool IsParseObjectGet(MethodCallExpression node) => node is { Object: { } } && typeof(ParseObject).GetTypeInfo().IsAssignableFrom(node.Object.Type.GetTypeInfo()) && node.Method.IsGenericMethod && node.Method.GetGenericMethodDefinition() == ParseObjectGetMethod; + + /// + /// Visits an Expression, converting ParseObject.Get/ParseObject[]/ParseObject.Property, + /// and nested indices into a single call to ParseObject.Get() with a "field path" like + /// "foo.bar.baz" + /// + class ObjectNormalizer : ExpressionVisitor + { + protected override Expression VisitIndex(IndexExpression node) + { + Expression visitedObject = Visit(node.Object); + MethodCallExpression indexer = visitedObject as MethodCallExpression; + + if (IsParseObjectGet(indexer)) + { + if (!(GetValue(node.Arguments[0]) is string indexValue)) + { + throw new InvalidOperationException("Index must be a string"); + } + + return Expression.Call(indexer.Object, ParseObjectGetMethod.MakeGenericMethod(node.Type), Expression.Constant($"{GetValue(indexer.Arguments[0])}.{indexValue}", typeof(string))); + } + + return base.VisitIndex(node); + } + + /// + /// Check for a ParseFieldName attribute and use that as the path component, turning + /// properties like foo.ObjectId into foo.Get("objectId") + /// + protected override Expression VisitMember(MemberExpression node) => node.Member.GetCustomAttribute() is { } fieldName && typeof(ParseObject).GetTypeInfo().IsAssignableFrom(node.Expression.Type.GetTypeInfo()) ? Expression.Call(node.Expression, ParseObjectGetMethod.MakeGenericMethod(node.Type), Expression.Constant(fieldName.FieldName, typeof(string))) : base.VisitMember(node); + + /// + /// If a ParseObject.Get() call has been cast, just change the generic parameter. + /// + protected override Expression VisitUnary(UnaryExpression node) + { + MethodCallExpression methodCall = Visit(node.Operand) as MethodCallExpression; + return (node.NodeType == ExpressionType.Convert || node.NodeType == ExpressionType.ConvertChecked) && IsParseObjectGet(methodCall) ? Expression.Call(methodCall.Object, ParseObjectGetMethod.MakeGenericMethod(node.Type), methodCall.Arguments) : base.VisitUnary(node); + } + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + // Turn parseObject["foo"] into parseObject.Get("foo") + + if (node.Method.Name == "get_Item" && node.Object is ParameterExpression) + { + return Expression.Call(node.Object, ParseObjectGetMethod.MakeGenericMethod(typeof(object)), Expression.Constant(GetValue(node.Arguments[0]) as string, typeof(string))); + } + + // Turn parseObject.Get("foo")["bar"] into parseObject.Get("foo.bar") + + if (node.Method.Name == "get_Item" || IsParseObjectGet(node)) + { + Expression visitedObject = Visit(node.Object); + MethodCallExpression indexer = visitedObject as MethodCallExpression; + + if (IsParseObjectGet(indexer)) + { + if (!(GetValue(node.Arguments[0]) is string indexValue)) + { + throw new InvalidOperationException("Index must be a string"); + } + + return Expression.Call(indexer.Object, ParseObjectGetMethod.MakeGenericMethod(node.Type), Expression.Constant($"{GetValue(indexer.Arguments[0])}.{indexValue}", typeof(string))); + } + } + + return base.VisitMethodCall(node); + } + } + + /// + /// Normalizes Where expressions. + /// + class WhereNormalizer : ExpressionVisitor + { + + /// + /// Normalizes binary operators. <, >, <=, >= !=, and == + /// This puts the ParseObject.Get() on the left side of the operation + /// (reversing it if necessary), and normalizes the ParseObject.Get() + /// + protected override Expression VisitBinary(BinaryExpression node) + { + MethodCallExpression rightTransformed = new ObjectNormalizer().Visit(node.Right) as MethodCallExpression, objectExpression; + Expression filterExpression; + bool inverted; + + if (new ObjectNormalizer().Visit(node.Left) is MethodCallExpression leftTransformed) + { + objectExpression = leftTransformed; + filterExpression = node.Right; + inverted = false; + } + else + { + objectExpression = rightTransformed; + filterExpression = node.Left; + inverted = true; + } + + try + { + switch (node.NodeType) + { + case ExpressionType.GreaterThan: + return inverted ? Expression.LessThan(objectExpression, filterExpression) : Expression.GreaterThan(objectExpression, filterExpression); + case ExpressionType.GreaterThanOrEqual: + return inverted ? Expression.LessThanOrEqual(objectExpression, filterExpression) : Expression.GreaterThanOrEqual(objectExpression, filterExpression); + case ExpressionType.LessThan: + return inverted ? Expression.GreaterThan(objectExpression, filterExpression) : Expression.LessThan(objectExpression, filterExpression); + case ExpressionType.LessThanOrEqual: + return inverted ? Expression.GreaterThanOrEqual(objectExpression, filterExpression) : Expression.LessThanOrEqual(objectExpression, filterExpression); + case ExpressionType.Equal: + return Expression.Equal(objectExpression, filterExpression); + case ExpressionType.NotEqual: + return Expression.NotEqual(objectExpression, filterExpression); + } + } + catch (ArgumentException) + { + throw new InvalidOperationException("Operation not supported: " + node); + } + + return base.VisitBinary(node); + } + + /// + /// If a ! operator is used, this removes the ! and instead calls the equivalent + /// function (so e.g. == becomes !=, < becomes >=, Contains becomes NotContains) + /// + protected override Expression VisitUnary(UnaryExpression node) + { + // This is incorrect because control is supposed to be able to flow out of the binaryOperand case if the value of NodeType is not matched against an ExpressionType value, which it will not do. + // + // return node switch + // { + // { NodeType: ExpressionType.Not, Operand: var operand } => Visit(operand) switch + // { + // BinaryExpression { Left: var left, Right: var right, NodeType: var type } binaryOperand => type switch + // { + // ExpressionType.GreaterThan => Expression.LessThanOrEqual(left, right), + // ExpressionType.GreaterThanOrEqual => Expression.LessThan(left, right), + // ExpressionType.LessThan => Expression.GreaterThanOrEqual(left, right), + // ExpressionType.LessThanOrEqual => Expression.GreaterThan(left, right), + // ExpressionType.Equal => Expression.NotEqual(left, right), + // ExpressionType.NotEqual => Expression.Equal(left, right), + // }, + // _ => base.VisitUnary(node) + // }, + // _ => base.VisitUnary(node) + // }; + + // Normalizes inversion + + if (node.NodeType == ExpressionType.Not) + { + Expression visitedOperand = Visit(node.Operand); + if (visitedOperand is BinaryExpression binaryOperand) + { + switch (binaryOperand.NodeType) + { + case ExpressionType.GreaterThan: + return Expression.LessThanOrEqual(binaryOperand.Left, binaryOperand.Right); + case ExpressionType.GreaterThanOrEqual: + return Expression.LessThan(binaryOperand.Left, binaryOperand.Right); + case ExpressionType.LessThan: + return Expression.GreaterThanOrEqual(binaryOperand.Left, binaryOperand.Right); + case ExpressionType.LessThanOrEqual: + return Expression.GreaterThan(binaryOperand.Left, binaryOperand.Right); + case ExpressionType.Equal: + return Expression.NotEqual(binaryOperand.Left, binaryOperand.Right); + case ExpressionType.NotEqual: + return Expression.Equal(binaryOperand.Left, binaryOperand.Right); + } + } + + if (visitedOperand is MethodCallExpression methodCallOperand) + { + if (methodCallOperand.Method.IsGenericMethod) + { + if (methodCallOperand.Method.GetGenericMethodDefinition() == ContainsMethod) + { + return Expression.Call(NotContainsMethod.MakeGenericMethod(methodCallOperand.Method.GetGenericArguments()), methodCallOperand.Arguments.ToArray()); + } + if (methodCallOperand.Method.GetGenericMethodDefinition() == NotContainsMethod) + { + return Expression.Call(ContainsMethod.MakeGenericMethod(methodCallOperand.Method.GetGenericArguments()), methodCallOperand.Arguments.ToArray()); + } + } + if (methodCallOperand.Method == ContainsKeyMethod) + { + return Expression.Call(NotContainsKeyMethod, methodCallOperand.Arguments.ToArray()); + } + if (methodCallOperand.Method == NotContainsKeyMethod) + { + return Expression.Call(ContainsKeyMethod, methodCallOperand.Arguments.ToArray()); + } + } + } + return base.VisitUnary(node); + } + + /// + /// Normalizes .Equals into == and Contains() into the appropriate stub. + /// + protected override Expression VisitMethodCall(MethodCallExpression node) + { + // Convert .Equals() into == + + if (node.Method.Name == "Equals" && node.Method.ReturnType == typeof(bool) && node.Method.GetParameters().Length == 1) + { + MethodCallExpression obj = new ObjectNormalizer().Visit(node.Object) as MethodCallExpression, parameter = new ObjectNormalizer().Visit(node.Arguments[0]) as MethodCallExpression; + + if (IsParseObjectGet(obj) && obj.Object is ParameterExpression || IsParseObjectGet(parameter) && parameter.Object is ParameterExpression) + { + return Expression.Equal(node.Object, node.Arguments[0]); + } + } + + // Convert the .Contains() into a ContainsStub + + if (node.Method != StringContainsMethod && node.Method.Name == "Contains" && node.Method.ReturnType == typeof(bool) && node.Method.GetParameters().Length <= 2) + { + Expression collection = node.Method.GetParameters().Length == 1 ? node.Object : node.Arguments[0]; + int parameterIndex = node.Method.GetParameters().Length - 1; + + if (new ObjectNormalizer().Visit(node.Arguments[parameterIndex]) is MethodCallExpression { } parameter && IsParseObjectGet(parameter) && parameter.Object is ParameterExpression) + { + return Expression.Call(ContainsMethod.MakeGenericMethod(parameter.Type), collection, parameter); + } + + if (new ObjectNormalizer().Visit(collection) is MethodCallExpression { } target && IsParseObjectGet(target) && target.Object is ParameterExpression) + { + Expression element = node.Arguments[parameterIndex]; + return Expression.Call(ContainsMethod.MakeGenericMethod(element.Type), target, element); + } + } + + // Convert obj["foo.bar"].ContainsKey("baz") into obj.ContainsKey("foo.bar.baz"). + + if (node.Method.Name == "ContainsKey" && node.Method.ReturnType == typeof(bool) && node.Method.GetParameters().Length == 1) + { + Expression target = null; + string path = null; + + if (new ObjectNormalizer().Visit(node.Object) is MethodCallExpression { } getter && IsParseObjectGet(getter) && getter.Object is ParameterExpression) + { + return Expression.Call(ContainsKeyMethod, getter.Object, Expression.Constant($"{GetValue(getter.Arguments[0])}.{GetValue(node.Arguments[0])}")); + } + else if (node.Object is ParameterExpression) + { + target = node.Object; + path = GetValue(node.Arguments[0]) as string; + } + + if (target is { } && path is { }) + { + return Expression.Call(ContainsKeyMethod, target, Expression.Constant(path)); + } + } + return base.VisitMethodCall(node); + } + } + + /// + /// Converts a normalized method call expression into the appropriate ParseQuery clause. + /// + static ParseQuery WhereMethodCall(this ParseQuery source, Expression> expression, MethodCallExpression node) where T : ParseObject + { + if (IsParseObjectGet(node) && (node.Type == typeof(bool) || node.Type == typeof(bool?))) + { + // This is a raw boolean field access like 'where obj.Get("foo")'. + + return source.WhereEqualTo(GetValue(node.Arguments[0]) as string, true); + } + + if (Mappings.TryGetValue(node.Method, out MethodInfo translatedMethod)) + { + MethodCallExpression objTransformed = new ObjectNormalizer().Visit(node.Object) as MethodCallExpression; + + if (!(IsParseObjectGet(objTransformed) && objTransformed.Object == expression.Parameters[0])) + { + throw new InvalidOperationException("The left-hand side of a supported function call must be a ParseObject field access."); + } + + return translatedMethod.DeclaringType.GetGenericTypeDefinition().MakeGenericType(typeof(T)).GetRuntimeMethod(translatedMethod.Name, translatedMethod.GetParameters().Select(parameter => parameter.ParameterType).ToArray()).Invoke(source, new[] { GetValue(objTransformed.Arguments[0]), GetValue(node.Arguments[0]) }) as ParseQuery; + } + + if (node.Arguments[0] == expression.Parameters[0]) + { + // obj.ContainsKey("foo") --> query.WhereExists("foo") + + if (node.Method == ContainsKeyMethod) + { + return source.WhereExists(GetValue(node.Arguments[1]) as string); + } + + // !obj.ContainsKey("foo") --> query.WhereDoesNotExist("foo") + + if (node.Method == NotContainsKeyMethod) + { + return source.WhereDoesNotExist(GetValue(node.Arguments[1]) as string); + } + } + + if (node.Method.IsGenericMethod) + { + if (node.Method.GetGenericMethodDefinition() == ContainsMethod) + { + // obj.Get>("path").Contains(someValue) + + if (IsParseObjectGet(node.Arguments[0] as MethodCallExpression)) + { + return source.WhereEqualTo(GetValue(((MethodCallExpression) node.Arguments[0]).Arguments[0]) as string, GetValue(node.Arguments[1])); + } + + // someList.Contains(obj.Get("path")) + + if (IsParseObjectGet(node.Arguments[1] as MethodCallExpression)) + { + return source.WhereContainedIn(GetValue(((MethodCallExpression) node.Arguments[1]).Arguments[0]) as string, (GetValue(node.Arguments[0]) as IEnumerable).Cast()); + } + } + + if (node.Method.GetGenericMethodDefinition() == NotContainsMethod) + { + // !obj.Get>("path").Contains(someValue) + + if (IsParseObjectGet(node.Arguments[0] as MethodCallExpression)) + { + return source.WhereNotEqualTo(GetValue(((MethodCallExpression) node.Arguments[0]).Arguments[0]) as string, GetValue(node.Arguments[1])); + } + + // !someList.Contains(obj.Get("path")) + + if (IsParseObjectGet(node.Arguments[1] as MethodCallExpression)) + { + return source.WhereNotContainedIn(GetValue(((MethodCallExpression) node.Arguments[1]).Arguments[0]) as string, (GetValue(node.Arguments[0]) as IEnumerable).Cast()); + } + } + } + throw new InvalidOperationException(node.Method + " is not a supported method call in a where expression."); + } + + /// + /// Converts a normalized binary expression into the appropriate ParseQuery clause. + /// + static ParseQuery WhereBinaryExpression(this ParseQuery source, Expression> expression, BinaryExpression node) where T : ParseObject + { + MethodCallExpression leftTransformed = new ObjectNormalizer().Visit(node.Left) as MethodCallExpression; + + if (!(IsParseObjectGet(leftTransformed) && leftTransformed.Object == expression.Parameters[0])) + { + throw new InvalidOperationException("Where expressions must have one side be a field operation on a ParseObject."); + } + + string fieldPath = GetValue(leftTransformed.Arguments[0]) as string; + object filterValue = GetValue(node.Right); + + if (filterValue != null && !ParseDataEncoder.Validate(filterValue)) + { + throw new InvalidOperationException("Where clauses must use types compatible with ParseObjects."); + } + + return node.NodeType switch + { + ExpressionType.GreaterThan => source.WhereGreaterThan(fieldPath, filterValue), + ExpressionType.GreaterThanOrEqual => source.WhereGreaterThanOrEqualTo(fieldPath, filterValue), + ExpressionType.LessThan => source.WhereLessThan(fieldPath, filterValue), + ExpressionType.LessThanOrEqual => source.WhereLessThanOrEqualTo(fieldPath, filterValue), + ExpressionType.Equal => source.WhereEqualTo(fieldPath, filterValue), + ExpressionType.NotEqual => source.WhereNotEqualTo(fieldPath, filterValue), + _ => throw new InvalidOperationException("Where expressions do not support this operator."), + }; + } + + /// + /// Filters a query based upon the predicate provided. + /// + /// The type of ParseObject being queried for. + /// The base to which + /// the predicate will be added. + /// A function to test each ParseObject for a condition. + /// The predicate must be able to be represented by one of the standard Where + /// functions on ParseQuery + /// A new ParseQuery whose results will match the given predicate as + /// well as the source's filters. + public static ParseQuery Where(this ParseQuery source, Expression> predicate) where TSource : ParseObject + { + // Handle top-level logic operators && and || + + if (predicate.Body is BinaryExpression binaryExpression) + { + if (binaryExpression.NodeType == ExpressionType.AndAlso) + { + return source.Where(Expression.Lambda>(binaryExpression.Left, predicate.Parameters)).Where(Expression.Lambda>(binaryExpression.Right, predicate.Parameters)); + } + + if (binaryExpression.NodeType == ExpressionType.OrElse) + { + return source.Services.ConstructOrQuery(source.Where(Expression.Lambda>(binaryExpression.Left, predicate.Parameters)), (ParseQuery) source.Where(Expression.Lambda>(binaryExpression.Right, predicate.Parameters))); + } + } + + Expression normalized = new WhereNormalizer().Visit(predicate.Body); + + if (normalized is MethodCallExpression methodCallExpr) + { + return source.WhereMethodCall(predicate, methodCallExpr); + } + + if (normalized is BinaryExpression binaryExpr) + { + return source.WhereBinaryExpression(predicate, binaryExpr); + } + + if (normalized is UnaryExpression { NodeType: ExpressionType.Not, Operand: MethodCallExpression { } node, Type: var type } unaryExpr && IsParseObjectGet(node) && (type == typeof(bool) || type == typeof(bool?))) + { + // This is a raw boolean field access like 'where !obj.Get("foo")'. + + return source.WhereNotEqualTo(GetValue(node.Arguments[0]) as string, true); + } + + throw new InvalidOperationException("Encountered an unsupported expression for ParseQueries."); + } + + /// + /// Normalizes an OrderBy's keySelector expression and then extracts the path + /// from the ParseObject.Get() call. + /// + static string GetOrderByPath(Expression> keySelector) + { + string result = null; + Expression normalized = new ObjectNormalizer().Visit(keySelector.Body); + MethodCallExpression callExpr = normalized as MethodCallExpression; + + if (IsParseObjectGet(callExpr) && callExpr.Object == keySelector.Parameters[0]) + { + // We're operating on the parameter + + result = GetValue(callExpr.Arguments[0]) as string; + } + + if (result == null) + { + throw new InvalidOperationException("OrderBy expression must be a field access on a ParseObject."); + } + + return result; + } + + /// + /// Orders a query based upon the key selector provided. + /// + /// The type of ParseObject being queried for. + /// The type of key returned by keySelector. + /// The query to order. + /// A function to extract a key from the ParseObject. + /// A new ParseQuery based on source whose results will be ordered by + /// the key specified in the keySelector. + public static ParseQuery OrderBy(this ParseQuery source, Expression> keySelector) where TSource : ParseObject => source.OrderBy(GetOrderByPath(keySelector)); + + /// + /// Orders a query based upon the key selector provided. + /// + /// The type of ParseObject being queried for. + /// The type of key returned by keySelector. + /// The query to order. + /// A function to extract a key from the ParseObject. + /// A new ParseQuery based on source whose results will be ordered by + /// the key specified in the keySelector. + public static ParseQuery OrderByDescending( this ParseQuery source, Expression> keySelector) where TSource : ParseObject => source.OrderByDescending(GetOrderByPath(keySelector)); + + /// + /// Performs a subsequent ordering of a query based upon the key selector provided. + /// + /// The type of ParseObject being queried for. + /// The type of key returned by keySelector. + /// The query to order. + /// A function to extract a key from the ParseObject. + /// A new ParseQuery based on source whose results will be ordered by + /// the key specified in the keySelector. + public static ParseQuery ThenBy(this ParseQuery source, Expression> keySelector) where TSource : ParseObject => source.ThenBy(GetOrderByPath(keySelector)); + + /// + /// Performs a subsequent ordering of a query based upon the key selector provided. + /// + /// The type of ParseObject being queried for. + /// The type of key returned by keySelector. + /// The query to order. + /// A function to extract a key from the ParseObject. + /// A new ParseQuery based on source whose results will be ordered by + /// the key specified in the keySelector. + public static ParseQuery ThenByDescending(this ParseQuery source, Expression> keySelector) where TSource : ParseObject => source.ThenByDescending(GetOrderByPath(keySelector)); + + /// + /// Correlates the elements of two queries based on matching keys. + /// + /// The type of ParseObjects of the first query. + /// The type of ParseObjects of the second query. + /// The type of the keys returned by the key selector + /// functions. + /// The type of the result. This must match either + /// TOuter or TInner + /// The first query to join. + /// The query to join to the first query. + /// A function to extract a join key from the results of + /// the first query. + /// A function to extract a join key from the results of + /// the second query. + /// A function to select either the outer or inner query + /// result to determine which query is the base query. + /// A new ParseQuery with a WhereMatchesQuery or WhereMatchesKeyInQuery + /// clause based upon the query indicated in the . + public static ParseQuery Join(this ParseQuery outer, ParseQuery inner, Expression> outerKeySelector, Expression> innerKeySelector, Expression> resultSelector) where TOuter : ParseObject where TInner : ParseObject where TResult : ParseObject + { + // resultSelector must select either the inner object or the outer object. If it's the inner object, reverse the query. + + if (resultSelector.Body == resultSelector.Parameters[1]) + { + // The inner object was selected. + + return inner.Join(outer, innerKeySelector, outerKeySelector, (i, o) => i) as ParseQuery; + } + + if (resultSelector.Body != resultSelector.Parameters[0]) + { + throw new InvalidOperationException("Joins must select either the outer or inner object."); + } + + // Normalize both selectors + Expression outerNormalized = new ObjectNormalizer().Visit(outerKeySelector.Body), innerNormalized = new ObjectNormalizer().Visit(innerKeySelector.Body); + MethodCallExpression outerAsGet = outerNormalized as MethodCallExpression, innerAsGet = innerNormalized as MethodCallExpression; + + if (IsParseObjectGet(outerAsGet) && outerAsGet.Object == outerKeySelector.Parameters[0]) + { + string outerKey = GetValue(outerAsGet.Arguments[0]) as string; + + if (IsParseObjectGet(innerAsGet) && innerAsGet.Object == innerKeySelector.Parameters[0]) + { + // Both are key accesses, so treat this as a WhereMatchesKeyInQuery. + + return outer.WhereMatchesKeyInQuery(outerKey, GetValue(innerAsGet.Arguments[0]) as string, inner) as ParseQuery; + } + + if (innerKeySelector.Body == innerKeySelector.Parameters[0]) + { + // The inner selector is on the result of the query itself, so treat this as a WhereMatchesQuery. + + return outer.WhereMatchesQuery(outerKey, inner) as ParseQuery; + } + + throw new InvalidOperationException("The key for the joined object must be a ParseObject or a field access on the ParseObject."); + } + + // TODO (hallucinogen): If we ever support "and" queries fully and/or support a "where this object + // matches some key in some other query" (as opposed to requiring a key on this query), we + // can add support for even more types of joins. + + throw new InvalidOperationException("The key for the selected object must be a field access on the ParseObject."); + } + + public static string GetClassName(this ParseQuery query) where T : ParseObject => query.ClassName; + + public static IDictionary BuildParameters(this ParseQuery query) where T : ParseObject => query.BuildParameters(false); + + public static object GetConstraint(this ParseQuery query, string key) where T : ParseObject => query.GetConstraint(key); + } +} diff --git a/Parse/Internal/Utilities/ParseRelationExtensions.cs b/Parse/Utilities/ParseRelationExtensions.cs similarity index 73% rename from Parse/Internal/Utilities/ParseRelationExtensions.cs rename to Parse/Utilities/ParseRelationExtensions.cs index 7f80ffb9..42bd5558 100644 --- a/Parse/Internal/Utilities/ParseRelationExtensions.cs +++ b/Parse/Utilities/ParseRelationExtensions.cs @@ -1,9 +1,6 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. -using System; -using System.Collections.Generic; - -namespace Parse.Core.Internal +namespace Parse.Abstractions.Internal { /// /// So here's the deal. We have a lot of internal APIs for ParseObject, ParseUser, etc. @@ -17,19 +14,10 @@ namespace Parse.Core.Internal /// public static class ParseRelationExtensions { - public static ParseRelation Create(ParseObject parent, string childKey) where T : ParseObject - { - return new ParseRelation(parent, childKey); - } + public static ParseRelation Create(ParseObject parent, string childKey) where T : ParseObject => new ParseRelation(parent, childKey); - public static ParseRelation Create(ParseObject parent, string childKey, string targetClassName) where T : ParseObject - { - return new ParseRelation(parent, childKey, targetClassName); - } + public static ParseRelation Create(ParseObject parent, string childKey, string targetClassName) where T : ParseObject => new ParseRelation(parent, childKey, targetClassName); - public static string GetTargetClassName(this ParseRelation relation) where T : ParseObject - { - return relation.TargetClassName; - } + public static string GetTargetClassName(this ParseRelation relation) where T : ParseObject => relation.TargetClassName; } } diff --git a/Parse/Internal/Utilities/ParseSessionExtensions.cs b/Parse/Utilities/ParseUserExtensions.cs similarity index 53% rename from Parse/Internal/Utilities/ParseSessionExtensions.cs rename to Parse/Utilities/ParseUserExtensions.cs index 131cafca..30e3bd87 100644 --- a/Parse/Internal/Utilities/ParseSessionExtensions.cs +++ b/Parse/Utilities/ParseUserExtensions.cs @@ -1,11 +1,10 @@ // Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. -using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace Parse.Core.Internal +namespace Parse.Abstractions.Internal { /// /// So here's the deal. We have a lot of internal APIs for ParseObject, ParseUser, etc. @@ -17,16 +16,14 @@ namespace Parse.Core.Internal /// So this class contains a bunch of extension methods that can live inside another /// namespace, which 'wrap' the intenral APIs that already exist. /// - public static class ParseSessionExtensions + public static class ParseUserExtensions { - public static Task UpgradeToRevocableSessionAsync(string sessionToken, CancellationToken cancellationToken) - { - return ParseSession.UpgradeToRevocableSessionAsync(sessionToken, cancellationToken); - } + public static Task UnlinkFromAsync(this ParseUser user, string authType, CancellationToken cancellationToken) => user.UnlinkFromAsync(authType, cancellationToken); - public static Task RevokeAsync(string sessionToken, CancellationToken cancellationToken) - { - return ParseSession.RevokeAsync(sessionToken, cancellationToken); - } + public static Task LinkWithAsync(this ParseUser user, string authType, CancellationToken cancellationToken) => user.LinkWithAsync(authType, cancellationToken); + + public static Task LinkWithAsync(this ParseUser user, string authType, IDictionary data, CancellationToken cancellationToken) => user.LinkWithAsync(authType, data, cancellationToken); + + public static Task UpgradeToRevocableSessionAsync(this ParseUser user, CancellationToken cancellationToken) => user.UpgradeToRevocableSessionAsync(cancellationToken); } } diff --git a/Parse/Utilities/PushServiceExtensions.cs b/Parse/Utilities/PushServiceExtensions.cs new file mode 100644 index 00000000..ad94e282 --- /dev/null +++ b/Parse/Utilities/PushServiceExtensions.cs @@ -0,0 +1,202 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure; +using Parse.Infrastructure.Utilities; +using Parse.Platform.Push; + +namespace Parse +{ + public static class PushServiceExtensions + { + /// + /// Pushes a simple message to every device. This is shorthand for: + /// + /// + /// var push = new ParsePush(); + /// push.Data = new Dictionary<string, object>{{"alert", alert}}; + /// return push.SendAsync(); + /// + /// + /// The alert message to send. + public static Task SendPushAlertAsync(this IServiceHub serviceHub, string alert) => new ParsePush(serviceHub) { Alert = alert }.SendAsync(); + + /// + /// Pushes a simple message to every device subscribed to channel. This is shorthand for: + /// + /// + /// var push = new ParsePush(); + /// push.Channels = new List<string> { channel }; + /// push.Data = new Dictionary<string, object>{{"alert", alert}}; + /// return push.SendAsync(); + /// + /// + /// The alert message to send. + /// An Installation must be subscribed to channel to receive this Push Notification. + public static Task SendPushAlertAsync(this IServiceHub serviceHub, string alert, string channel) => new ParsePush(serviceHub) { Channels = new List { channel }, Alert = alert }.SendAsync(); + + /// + /// Pushes a simple message to every device subscribed to any of channels. This is shorthand for: + /// + /// + /// var push = new ParsePush(); + /// push.Channels = channels; + /// push.Data = new Dictionary<string, object>{{"alert", alert}}; + /// return push.SendAsync(); + /// + /// + /// The alert message to send. + /// An Installation must be subscribed to any of channels to receive this Push Notification. + public static Task SendPushAlertAsync(this IServiceHub serviceHub, string alert, IEnumerable channels) => new ParsePush(serviceHub) { Channels = channels, Alert = alert }.SendAsync(); + + /// + /// Pushes a simple message to every device matching the target query. This is shorthand for: + /// + /// + /// var push = new ParsePush(); + /// push.Query = query; + /// push.Data = new Dictionary<string, object>{{"alert", alert}}; + /// return push.SendAsync(); + /// + /// + /// The alert message to send. + /// A query filtering the devices which should receive this Push Notification. + public static Task SendPushAlertAsync(this IServiceHub serviceHub, string alert, ParseQuery query) => new ParsePush(serviceHub) { Query = query, Alert = alert }.SendAsync(); + + /// + /// Pushes an arbitrary payload to every device. This is shorthand for: + /// + /// + /// var push = new ParsePush(); + /// push.Data = data; + /// return push.SendAsync(); + /// + /// + /// A push payload. See the ParsePush.Data property for more information. + public static Task SendPushDataAsync(this IServiceHub serviceHub, IDictionary data) => new ParsePush(serviceHub) { Data = data }.SendAsync(); + + /// + /// Pushes an arbitrary payload to every device subscribed to channel. This is shorthand for: + /// + /// + /// var push = new ParsePush(); + /// push.Channels = new List<string> { channel }; + /// push.Data = data; + /// return push.SendAsync(); + /// + /// + /// A push payload. See the ParsePush.Data property for more information. + /// An Installation must be subscribed to channel to receive this Push Notification. + public static Task SendPushDataAsync(this IServiceHub serviceHub, IDictionary data, string channel) => new ParsePush(serviceHub) { Channels = new List { channel }, Data = data }.SendAsync(); + + /// + /// Pushes an arbitrary payload to every device subscribed to any of channels. This is shorthand for: + /// + /// + /// var push = new ParsePush(); + /// push.Channels = channels; + /// push.Data = data; + /// return push.SendAsync(); + /// + /// + /// A push payload. See the ParsePush.Data property for more information. + /// An Installation must be subscribed to any of channels to receive this Push Notification. + public static Task SendPushDataAsync(this IServiceHub serviceHub, IDictionary data, IEnumerable channels) => new ParsePush(serviceHub) { Channels = channels, Data = data }.SendAsync(); + + /// + /// Pushes an arbitrary payload to every device matching target. This is shorthand for: + /// + /// + /// var push = new ParsePush(); + /// push.Query = query + /// push.Data = data; + /// return push.SendAsync(); + /// + /// + /// A push payload. See the ParsePush.Data property for more information. + /// A query filtering the devices which should receive this Push Notification. + public static Task SendPushDataAsync(this IServiceHub serviceHub, IDictionary data, ParseQuery query) => new ParsePush(serviceHub) { Query = query, Data = data }.SendAsync(); + + #region Receiving Push + +#warning Check if this should be moved into IParsePushController. + + /// + /// An event fired when a push notification is received. + /// + public static event EventHandler ParsePushNotificationReceived + { + add + { + parsePushNotificationReceived.Add(value); + } + remove + { + parsePushNotificationReceived.Remove(value); + } + } + + internal static readonly SynchronizedEventHandler parsePushNotificationReceived = new SynchronizedEventHandler(); + + #endregion + + #region Push Subscription + + /// + /// Subscribe the current installation to this channel. This is shorthand for: + /// + /// + /// var installation = ParseInstallation.CurrentInstallation; + /// installation.AddUniqueToList("channels", channel); + /// installation.SaveAsync(cancellationToken); + /// + /// + /// The channel to which this installation should subscribe. + /// CancellationToken to cancel the current operation. + public static Task SubscribeToPushChannelAsync(this IServiceHub serviceHub, string channel, CancellationToken cancellationToken = default) => SubscribeToPushChannelsAsync(serviceHub, new List { channel }, cancellationToken); + + /// + /// Subscribe the current installation to these channels. This is shorthand for: + /// + /// + /// var installation = ParseInstallation.CurrentInstallation; + /// installation.AddRangeUniqueToList("channels", channels); + /// installation.SaveAsync(cancellationToken); + /// + /// + /// The channels to which this installation should subscribe. + /// CancellationToken to cancel the current operation. + public static Task SubscribeToPushChannelsAsync(this IServiceHub serviceHub, IEnumerable channels, CancellationToken cancellationToken = default) => serviceHub.PushChannelsController.SubscribeAsync(channels, serviceHub, cancellationToken); + + /// + /// Unsubscribe the current installation from this channel. This is shorthand for: + /// + /// + /// var installation = ParseInstallation.CurrentInstallation; + /// installation.Remove("channels", channel); + /// installation.SaveAsync(cancellationToken); + /// + /// + /// The channel from which this installation should unsubscribe. + /// CancellationToken to cancel the current operation. + public static Task UnsubscribeToPushChannelAsync(this IServiceHub serviceHub, string channel, CancellationToken cancellationToken = default) => UnsubscribeToPushChannelsAsync(serviceHub, new List { channel }, cancellationToken); + + /// + /// Unsubscribe the current installation from these channels. This is shorthand for: + /// + /// + /// var installation = ParseInstallation.CurrentInstallation; + /// installation.RemoveAllFromList("channels", channels); + /// installation.SaveAsync(cancellationToken); + /// + /// + /// The channels from which this installation should unsubscribe. + /// CancellationToken to cancel the current operation. + public static Task UnsubscribeToPushChannelsAsync(this IServiceHub serviceHub, IEnumerable channels, CancellationToken cancellationToken = default) => serviceHub.PushChannelsController.UnsubscribeAsync(channels, serviceHub, cancellationToken); + + #endregion + } +} diff --git a/Parse/Utilities/QueryServiceExtensions.cs b/Parse/Utilities/QueryServiceExtensions.cs new file mode 100644 index 00000000..c31b66e6 --- /dev/null +++ b/Parse/Utilities/QueryServiceExtensions.cs @@ -0,0 +1,67 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Parse.Abstractions.Infrastructure; + +namespace Parse +{ + public static class QueryServiceExtensions + { + public static ParseQuery GetQuery(this IServiceHub serviceHub) where T : ParseObject => new ParseQuery(serviceHub); + + // ALTERNATE NAME: BuildOrQuery + + /// + /// Constructs a query that is the or of the given queries. + /// + /// The type of ParseObject being queried. + /// An initial query to 'or' with additional queries. + /// The list of ParseQueries to 'or' together. + /// A query that is the or of the given queries. + public static ParseQuery ConstructOrQuery(this IServiceHub serviceHub, ParseQuery source, params ParseQuery[] queries) where T : ParseObject => serviceHub.ConstructOrQuery(queries.Concat(new[] { source })); + + /// + /// Constructs a query that is the or of the given queries. + /// + /// The list of ParseQueries to 'or' together. + /// A ParseQquery that is the 'or' of the passed in queries. + public static ParseQuery ConstructOrQuery(this IServiceHub serviceHub, IEnumerable> queries) where T : ParseObject + { + string className = default; + List> orValue = new List> { }; + + // We need to cast it to non-generic IEnumerable because of AOT-limitation + + IEnumerable nonGenericQueries = queries; + foreach (object obj in nonGenericQueries) + { + ParseQuery query = obj as ParseQuery; + + if (className is { } && query.ClassName != className) + { + throw new ArgumentException("All of the queries in an or query must be on the same class."); + } + + className = query.ClassName; + IDictionary parameters = query.BuildParameters(); + + if (parameters.Count == 0) + { + continue; + } + + if (!parameters.TryGetValue("where", out object where) || parameters.Count > 1) + { + throw new ArgumentException("None of the queries in an or query can have non-filtering clauses"); + } + + orValue.Add(where as IDictionary); + } + + return new ParseQuery(new ParseQuery(serviceHub, className), where: new Dictionary { ["$or"] = orValue }); + } + } +} diff --git a/Parse/Utilities/RoleServiceExtensions.cs b/Parse/Utilities/RoleServiceExtensions.cs new file mode 100644 index 00000000..fcd5d05a --- /dev/null +++ b/Parse/Utilities/RoleServiceExtensions.cs @@ -0,0 +1,14 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using Parse.Abstractions.Infrastructure; + +namespace Parse +{ + public static class RoleServiceExtensions + { + /// + /// Gets a over the Role collection. + /// + public static ParseQuery GetRoleQuery(this IServiceHub serviceHub) => serviceHub.GetQuery(); + } +} diff --git a/Parse/Utilities/SessionsServiceExtensions.cs b/Parse/Utilities/SessionsServiceExtensions.cs new file mode 100644 index 00000000..a23aac95 --- /dev/null +++ b/Parse/Utilities/SessionsServiceExtensions.cs @@ -0,0 +1,37 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure; +using Parse.Infrastructure.Utilities; + +namespace Parse +{ + public static class SessionsServiceExtensions + { + /// + /// Constructs a for ParseSession. + /// + public static ParseQuery GetSessionQuery(this IServiceHub serviceHub) => serviceHub.GetQuery(); + + /// + /// Gets the current object related to the current user. + /// + public static Task GetCurrentSessionAsync(this IServiceHub serviceHub) => GetCurrentSessionAsync(serviceHub, CancellationToken.None); + + /// + /// Gets the current object related to the current user. + /// + /// The cancellation token + public static Task GetCurrentSessionAsync(this IServiceHub serviceHub, CancellationToken cancellationToken) => serviceHub.GetCurrentUserAsync().OnSuccess(task => task.Result switch + { + null => Task.FromResult(default), + { SessionToken: null } => Task.FromResult(default), + { SessionToken: { } sessionToken } => serviceHub.SessionController.GetSessionAsync(sessionToken, serviceHub, cancellationToken).OnSuccess(successTask => serviceHub.GenerateObjectFromState(successTask.Result, "_Session")) + }).Unwrap(); + + public static Task RevokeSessionAsync(this IServiceHub serviceHub, string sessionToken, CancellationToken cancellationToken) => sessionToken is null || !serviceHub.SessionController.IsRevocableSessionToken(sessionToken) ? Task.CompletedTask : serviceHub.SessionController.RevokeAsync(sessionToken, cancellationToken); + + public static Task UpgradeToRevocableSessionAsync(this IServiceHub serviceHub, string sessionToken, CancellationToken cancellationToken) => sessionToken is null || serviceHub.SessionController.IsRevocableSessionToken(sessionToken) ? Task.FromResult(sessionToken) : serviceHub.SessionController.UpgradeToRevocableSessionAsync(sessionToken, serviceHub, cancellationToken).OnSuccess(task => serviceHub.GenerateObjectFromState(task.Result, "_Session").SessionToken); + } +} diff --git a/Parse/Utilities/UserServiceExtensions.cs b/Parse/Utilities/UserServiceExtensions.cs new file mode 100644 index 00000000..c4188193 --- /dev/null +++ b/Parse/Utilities/UserServiceExtensions.cs @@ -0,0 +1,240 @@ +// Copyright (c) 2015-present, Parse, LLC. All rights reserved. This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree. An additional grant of patent rights can be found in the PATENTS file in the same directory. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Parse.Abstractions.Infrastructure; +using Parse.Abstractions.Internal; +using Parse.Abstractions.Platform.Authentication; +using Parse.Infrastructure.Utilities; + +namespace Parse +{ + public static class UserServiceExtensions + { + internal static string GetCurrentSessionToken(this IServiceHub serviceHub) + { + Task sessionTokenTask = GetCurrentSessionTokenAsync(serviceHub); + sessionTokenTask.Wait(); + return sessionTokenTask.Result; + } + + internal static Task GetCurrentSessionTokenAsync(this IServiceHub serviceHub, CancellationToken cancellationToken = default) => serviceHub.CurrentUserController.GetCurrentSessionTokenAsync(serviceHub, cancellationToken); + + // TODO: Consider renaming SignUpAsync and LogInAsync to SignUpWithAsync and LogInWithAsync, respectively. + // TODO: Consider returning the created user from the SignUpAsync overload that accepts a username and password. + + /// + /// Creates a new , saves it with the target Parse Server instance, and then authenticates it on the target client. + /// + /// The instance to target when creating the user and authenticating. + /// The value that should be used for . + /// The value that should be used for . + /// The cancellation token. + public static Task SignUpAsync(this IServiceHub serviceHub, string username, string password, CancellationToken cancellationToken = default) => new ParseUser { Services = serviceHub, Username = username, Password = password }.SignUpAsync(cancellationToken); + + /// + /// Saves the provided instance with the target Parse Server instance and then authenticates it on the target client. This method should only be used once has been called and is the wanted bind target, or if has already been set or has already been called on the . + /// + /// The instance to target when creating the user and authenticating. + /// The instance to save on the target Parse Server instance and authenticate. + /// The cancellation token. + public static Task SignUpAsync(this IServiceHub serviceHub, ParseUser user, CancellationToken cancellationToken = default) + { + user.Bind(serviceHub); + return user.SignUpAsync(cancellationToken); + } + + /// + /// Logs in a user with a username and password. On success, this saves the session to disk or to memory so you can retrieve the currently logged in user using . + /// + /// The instance to target when logging in. + /// The username to log in with. + /// The password to log in with. + /// The cancellation token. + /// The newly logged-in user. + public static Task LogInAsync(this IServiceHub serviceHub, string username, string password, CancellationToken cancellationToken = default) => serviceHub.UserController.LogInAsync(username, password, serviceHub, cancellationToken).OnSuccess(task => + { + ParseUser user = serviceHub.GenerateObjectFromState(task.Result, "_User"); + return SaveCurrentUserAsync(serviceHub, user).OnSuccess(_ => user); + }).Unwrap(); + + /// + /// Logs in a user with a username and password. On success, this saves the session to disk so you + /// can retrieve the currently logged in user using . + /// + /// The session token to authorize with + /// The cancellation token. + /// The user if authorization was successful + public static Task BecomeAsync(this IServiceHub serviceHub, string sessionToken, CancellationToken cancellationToken = default) => serviceHub.UserController.GetUserAsync(sessionToken, serviceHub, cancellationToken).OnSuccess(t => + { + ParseUser user = serviceHub.GenerateObjectFromState(t.Result, "_User"); + return SaveCurrentUserAsync(serviceHub, user).OnSuccess(_ => user); + }).Unwrap(); + + /// + /// Logs out the currently logged in user session. This will remove the session from disk, log out of + /// linked services, and future calls to will return null. + /// + /// + /// Typically, you should use , unless you are managing your own threading. + /// + public static void LogOut(this IServiceHub serviceHub) => LogOutAsync(serviceHub).Wait(); // TODO (hallucinogen): this will without a doubt fail in Unity. But what else can we do? + + /// + /// Logs out the currently logged in user session. This will remove the session from disk, log out of + /// linked services, and future calls to will return null. + /// + /// + /// This is preferable to using , unless your code is already running from a + /// background thread. + /// + public static Task LogOutAsync(this IServiceHub serviceHub) => LogOutAsync(serviceHub, CancellationToken.None); + + /// + /// Logs out the currently logged in user session. This will remove the session from disk, log out of + /// linked services, and future calls to will return null. + /// + /// This is preferable to using , unless your code is already running from a + /// background thread. + /// + public static Task LogOutAsync(this IServiceHub serviceHub, CancellationToken cancellationToken) => GetCurrentUserAsync(serviceHub).OnSuccess(task => + { + LogOutWithProviders(); + return task.Result is { } user ? user.TaskQueue.Enqueue(toAwait => user.LogOutAsync(toAwait, cancellationToken), cancellationToken) : Task.CompletedTask; + }).Unwrap(); + + static void LogOutWithProviders() + { + foreach (IParseAuthenticationProvider provider in ParseUser.Authenticators.Values) + { + provider.Deauthenticate(); + } + } + + /// + /// Gets the currently logged in ParseUser with a valid session, either from memory or disk + /// if necessary. + /// + public static ParseUser GetCurrentUser(this IServiceHub serviceHub) + { + Task userTask = GetCurrentUserAsync(serviceHub); + + // TODO (hallucinogen): this will without a doubt fail in Unity. How should we fix it? + + userTask.Wait(); + return userTask.Result; + } + + /// + /// Gets the currently logged in ParseUser with a valid session, either from memory or disk + /// if necessary, asynchronously. + /// + internal static Task GetCurrentUserAsync(this IServiceHub serviceHub, CancellationToken cancellationToken = default) => serviceHub.CurrentUserController.GetAsync(serviceHub, cancellationToken); + + internal static Task SaveCurrentUserAsync(this IServiceHub serviceHub, ParseUser user, CancellationToken cancellationToken = default) => serviceHub.CurrentUserController.SetAsync(user, cancellationToken); + + internal static void ClearInMemoryUser(this IServiceHub serviceHub) => serviceHub.CurrentUserController.ClearFromMemory(); + + /// + /// Constructs a for s. + /// + public static ParseQuery GetUserQuery(this IServiceHub serviceHub) => serviceHub.GetQuery(); + + #region Legacy / Revocable Session Tokens + + /// + /// Tells server to use revocable session on LogIn and SignUp, even when App's Settings + /// has "Require Revocable Session" turned off. Issues network request in background to + /// migrate the sessionToken on disk to revocable session. + /// + /// The Task that upgrades the session. + public static Task EnableRevocableSessionAsync(this IServiceHub serviceHub, CancellationToken cancellationToken = default) + { + lock (serviceHub.UserController.RevocableSessionEnabledMutex) + { + serviceHub.UserController.RevocableSessionEnabled = true; + } + + return GetCurrentUserAsync(serviceHub, cancellationToken).OnSuccess(task => task.Result.UpgradeToRevocableSessionAsync(cancellationToken)); + } + + internal static void DisableRevocableSession(this IServiceHub serviceHub) + { + lock (serviceHub.UserController.RevocableSessionEnabledMutex) + { + serviceHub.UserController.RevocableSessionEnabled = false; + } + } + + internal static bool GetIsRevocableSessionEnabled(this IServiceHub serviceHub) + { + lock (serviceHub.UserController.RevocableSessionEnabledMutex) + { + return serviceHub.UserController.RevocableSessionEnabled; + } + } + + #endregion + + /// + /// Requests a password reset email to be sent to the specified email address associated with the + /// user account. This email allows the user to securely reset their password on the Parse site. + /// + /// The email address associated with the user that forgot their password. + public static Task RequestPasswordResetAsync(this IServiceHub serviceHub, string email) => RequestPasswordResetAsync(serviceHub, email, CancellationToken.None); + + /// + /// Requests a password reset email to be sent to the specified email address associated with the + /// user account. This email allows the user to securely reset their password on the Parse site. + /// + /// The email address associated with the user that forgot their password. + /// The cancellation token. + public static Task RequestPasswordResetAsync(this IServiceHub serviceHub, string email, CancellationToken cancellationToken) => serviceHub.UserController.RequestPasswordResetAsync(email, cancellationToken); + + public static Task LogInWithAsync(this IServiceHub serviceHub, string authType, IDictionary data, CancellationToken cancellationToken) + { + ParseUser user = null; + + return serviceHub.UserController.LogInAsync(authType, data, serviceHub, cancellationToken).OnSuccess(task => + { + user = serviceHub.GenerateObjectFromState(task.Result, "_User"); + + lock (user.Mutex) + { + if (user.AuthData == null) + { + user.AuthData = new Dictionary>(); + } + + user.AuthData[authType] = data; + +#warning Check if SynchronizeAllAuthData should accept an IServiceHub for consistency on which actions take place on which IServiceHub implementation instance. + + user.SynchronizeAllAuthData(); + } + + return SaveCurrentUserAsync(serviceHub, user); + }).Unwrap().OnSuccess(t => user); + } + + public static Task LogInWithAsync(this IServiceHub serviceHub, string authType, CancellationToken cancellationToken) + { + IParseAuthenticationProvider provider = ParseUser.GetProvider(authType); + return provider.AuthenticateAsync(cancellationToken).OnSuccess(authData => LogInWithAsync(serviceHub, authType, authData.Result, cancellationToken)).Unwrap(); + } + + internal static void RegisterProvider(this IServiceHub serviceHub, IParseAuthenticationProvider provider) + { + ParseUser.Authenticators[provider.AuthType] = provider; + ParseUser curUser = GetCurrentUser(serviceHub); + + if (curUser != null) + { +#warning Check if SynchronizeAllAuthData should accept an IServiceHub for consistency on which actions take place on which IServiceHub implementation instance. + + curUser.SynchronizeAuthData(provider); + } + } + } +} diff --git a/README.md b/README.md index 8b5dbd81..a1a3dbd4 100644 --- a/README.md +++ b/README.md @@ -9,51 +9,166 @@ ![Twitter Follow](https://img.shields.io/twitter/follow/ParsePlatform.svg?label=Follow%20us%20on%20Twitter&style=social) ## Getting Started -The latest stable release of the SDK is available as a [NuGet package][nuget-link]. Note that the latest package currently available on the official distribution channel is quite old. -To use the most up-to-date code, build this project and reference the generated NuGet package. +The latest stable release of the SDK is available as [a NuGet package][nuget-link]. Note that the latest package currently available on the official distribution channel is quite old; to use the most up-to-date code, build this project and reference the generated NuGet package. ## Using the Code -Make sure you are using the project's root namespace: +Make sure you are using the project's root namespace. -```cs +```csharp using Parse; ``` -Then, in your program's entry point, paste the following code, with the text reflecting your application and Parse Server setup emplaced between the quotation marks. +The `ParseClient` class has three constructors, one allowing you to specify your Application ID, Server URI, and .NET Key as well as some configuration items, one for creating clones of `ParseClient.Instance` if `Publicize` was called on an instance previously, and another, accepting an `IServerConnectionData` implementation instance which exposes the data needed for the SDK to connect to a Parse Server instance, like the first constructor, but with a few extra options. You can create your own `IServerConnectionData` implementation or use the `ServerConnectionData` struct. `IServerConnectionData` allows you to expose a value the SDK should use as the master key, as well as some extra request headers if needed. -```cs -ParseClient.Initialize(new ParseClient.Configuration +```csharp +ParseClient client = new ParseClient("Your Application ID", "The Parse Server Instance Host URI", "Your .NET Key"); +``` + +```csharp +ParseClient client = new ParseClient(new ServerConnectionData { - ApplicationId = "", - Key = "", - Server = "", + ApplicationID = "Your Application ID", + ServerURI = "The Parse Server Instance Host URI", + Key = "Your .NET Key", // This is unnecessary if a value for MasterKey is specified. + MasterKey = "Your Master Key", + Headers = new Dictionary + { + ["X-Extra-Header"] = "Some Value" + } }); ``` -`ApplicationId` is your app's `ApplicationId` field from your Parse Server. -`Key` is your app's `DotNetKey` field from your Parse Server. -`Server` is the full URL to your web-hosted Parse Server. - -If you would like to, you can also set the `MasterKey` property, which will allow the SDK to bypass any CLPs and object permissions that are set. This property should be compatible with read-only master keys as well. +`ServerConnectionData` is available in the `Parse.Infrastructure` namespace. -There are also a few optional parameters you can choose to set if you prefer or are experiencing issues with the SDK; sometimes the operation that generates values for these properties automatically can fail unexpectedly, causing the SDK to not be able to initialize, so these properties are provided to give you the ability to bypass that operation by providing the details outright. +The two non-cloning `ParseClient` constructors contain optional parameters for an `IServiceHub` implementation instance and an array of `IServiceHubMutator`s. These should only be used when the behaviour of the SDK needs to be changed such as [when it is used with the Unity game engine](#use-in-unity-client). -`StorageConfiguration` represents some metadata information usually collected reflectively about the project for the purpose of data caching. -`VersionInfo` represents some version information usually collected reflectively about the project for the purposes of data caching and metadata collection for installation object creation. To find full usage instructions for the latest stable release, please visit the [Parse docs website][parse-docs-link]. Please note that the latest stable release is quite old and does not reflect the work being done at the moment. -## Building The Library -You can build the library from Visual Studio Code (with the proper extensions), Visual Studio 2017 Community and higher, or Visual Studio for Mac 7 and higher. You can also build the library using the command line: +### Common Definitions + +- `Application ID`: +Your app's `ApplicationId` field from your Parse Server. +- `Key`: +Your app's `.NET Key` field from your Parse Server. +- `Master Key`: +Your app's `Master Key` field from your Parse Server. Using this key with the SDK will allow it to bypass any CLPs and object permissions that are set. This also should be compatible with read-only master keys as well. +- `Server URI`: +The full URL to your web-hosted Parse Server. + +### Client-Side Use + +In your program's entry point, instantiate a `ParseClient` with all the parameters needed to connect to your target Parse Server instance, then call `Publicize` on it. This will light up the static `ParseClient.Instance` property with the newly-instantiated `ParseClient`, so you can perform operations with the SDK. + +```csharp +new ParseClient(/* Parameters */).Publicize(); +``` + +### Use In Unity Client + +In Unity, the same logic applies to use the SDK as in [any other client](#client-side-use), except that a special `IServiceHub` impelementation instance, a `MetadataMutator`, and an `AbsoluteCacheLocationMutator` need to be passed in to one of the non-cloning `ParseClient` constructors in order to specify the environment and platform metadata, as well as the absolute cache location manually. This step is needed because the logic that creates these values automatically will fail and create incorrect values. The functionality to do this automatically may eventually be provided as a Unity package in the future, but for now, the following code can be used. + +```csharp +using System; +using UnityEngine; +using Parse.Infrastructure; +``` + +```csharp +new ParseClient(/* Parameters */, new LateInitializedMutableServiceHub { }, new MetadataMutator { EnvironmentData = new EnvironmentData { OSVersion = SystemInfo.operatingSystem, Platform = $"Unity {Application.unityVersion} on {SystemInfo.operatingSystemFamily}", TimeZone = TimeZoneInfo.Local.StandardName }, HostManifestData = new HostManifestData { Name = Application.productName, Identifier = Application.productName, ShortVersion = Application.version, Version = Application.version } }, new AbsoluteCacheLocationMutator { CustomAbsoluteCacheFilePath = $"{Application.persistentDataPath.Replace('/', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}Parse.cache" }).Publicize(); +``` + +Other `IServiceHubMutator` implementations are available that do different things, such as the `RelativeCacheLocationMutator`, which allows a custom cache location relative to the default base folder (`System.Environment.SpecialFolder.LocalApplicationData`) to be specified. + +If you are having trouble getting the SDK to work on other platforms, try to use the above code to control what values for various metadata information items the SDK will use, to see if that fixes the issue. + +### Server-Side Use + +The SDK can be set up in a way such that every new `ParseClient` instance can authenticate a different user concurrently. This is enabled by an `IServiceHubMutator` implementation which adds itself as an `IServiceHubCloner` implementation to the service hub which, making it so that consecutive calls to the cloning `ParseClient` constructor (the one without parameters) will clone the publicized `ParseClient` instance, exposed by `ParseClient.Instance`, replacing the `IParseCurrentUserController` implementation instance with a fresh one with no caching every time. This allows you to configure the original instance, and have the clones retain the general behaviour, while also allowing the differnt users to be signed into the their respective clones and execute requests concurrently, without causing race conditions. To use this feature of the SDK, the first `ParseClient` instance must be constructued and publicized as follows once, before any other `ParseClient` instantiations. Any classes that need to be registered must be done so with the original instance. + +```csharp +new ParseClient(/* Parameters */, default, new ConcurrentUserServiceHubCloner { }).Publicize(); +``` + +Consecutive instantiations can be done via the cloning constructor for simplicity's sake. + +```csharp +ParseClient client = new ParseClient { }; +``` + +### Basic Demonstration + +The following code shows how to use the Parse .NET SDK to create a new user, save and authenticate the user, deauthenticate the user, re-authenticate the user, create an object with permissions that allow only the user to modify it, save the object, update the object, delete the object, and deauthenticate the user once more. + +```csharp +// Instantiate a ParseClient. +ParseClient client = new ParseClient(/* Parameters */); + +// Create a user, save it, and authenticate with it. +await client.SignUpAsync(username: "Test", password: "Test"); + +// Get the authenticated user. This is can also be done with a variable that stores the ParseUser instance before the SignUp overload that accepts a ParseUser is called. +Console.WriteLine(client.GetCurrentUser().SessionToken); + +// Deauthenticate the user. +await client.LogOutAsync(); + +// Authenticate the user. +ParseUser user = await client.LogInAsync(username: "Test", password: "Test"); + +// Create a new object with permessions that allow only the user to modify it. +ParseObject testObject = new ParseObject("TestClass") { ACL = new ParseACL(user) }; + +// Bind the ParseObject to the target ParseClient instance. This is unnecessary if Publicize is called on the client. +testObject.Bind(client); + +// Set some value on the object. +testObject.Set("someValue", "This is a value."); + +// See that the ObjectId of an unsaved object is null; +Console.WriteLine(testObject.ObjectId); + +// Save the object to the target Parse Server instance. +await testObject.SaveAsync(); + +// See that the ObjectId of a saved object is non-null; +Console.WriteLine(testObject.ObjectId); + +// Query the object back down from the server to check that it was actually saved. +Console.WriteLine((await client.GetQuery("TestClass").WhereEqualTo("objectId", testObject.ObjectId).FirstAsync()).Get("someValue")); + +// Mutate some value on the object. +testObject.Set("someValue", "This is another value."); + +// Save the object again. +await testObject.SaveAsync(); + +// Query the object again to see that the change was made. +Console.WriteLine((await client.GetQuery("TestClass").WhereEqualTo("objectId", testObject.ObjectId).FirstAsync()).Get("someValue")); + +// Store the object's objectId so it can be verified that it was deleted later. +var testObjectId = testObject.ObjectId; + +// Delete the object. +await testObject.DeleteAsync(); + +// Check that the object was deleted from the server. +Console.WriteLine(await client.GetQuery("TestClass").WhereEqualTo("objectId", testObjectId).FirstOrDefaultAsync() == null); + +// Deauthenticate the user again. +await client.LogOutAsync(); +``` + +## Local Builds +You can build the SDK on any system with the MSBuild or .NET Core CLI installed. Results can be found under either the `Release/netstandard` or `Debug/netstandard` in the `bin` folder unless a non-standard build configuration is used. + +## .NET Core CLI -### On Windows or any .NET Core compatible Unix-based system with the .NET Core SDK installed: ```batch dotnet build Parse.sln ``` -Results can be found in either `Parse/bin/Release/netstandard2.0/` or `Parse/bin/Debug/netstandard2.0/` relative to the root project directory, where `/` is the path separator character for your system. - -## How Do I Contribute? +## Contributions We want to make contributing to this project as easy and transparent as possible. Please refer to the [Contribution Guidelines][contributing]. ## License @@ -72,4 +187,4 @@ of patent rights can be found in the PATENTS file in the same directory. [license-link]: https://github.com/parse-community/Parse-SDK-dotNET/blob/master/LICENSE [nuget-link]: http://nuget.org/packages/parse [nuget-svg]: https://img.shields.io/nuget/v/parse.svg - [parse-docs-link]: http://docs.parseplatform.org/ + [parse-docs-link]: http://docs.parseplatform.org/ \ No newline at end of file