From 6dd2c163c066a964f389b48ee64e3ef72a6f4335 Mon Sep 17 00:00:00 2001 From: ebragge Date: Thu, 10 Mar 2016 20:48:43 +0200 Subject: [PATCH] feat(AsyncStorageModule): add AsyncStorageModule AsyncStorageModule provides permanent storage for React Native JavaScript applications. - add AsyncStorageModuleTests Fixes #136 --- .../Storage/AsyncStorageModuleTests.cs | 195 +++++++++++++ ReactNative.Tests/ReactNative.Tests.csproj | 1 + .../Modules/Storage/AsyncStorageModule.cs | 264 +++++++++++++++++- 3 files changed, 455 insertions(+), 5 deletions(-) create mode 100644 ReactNative.Tests/Modules/Storage/AsyncStorageModuleTests.cs diff --git a/ReactNative.Tests/Modules/Storage/AsyncStorageModuleTests.cs b/ReactNative.Tests/Modules/Storage/AsyncStorageModuleTests.cs new file mode 100644 index 00000000000..93816df352f --- /dev/null +++ b/ReactNative.Tests/Modules/Storage/AsyncStorageModuleTests.cs @@ -0,0 +1,195 @@ +using Microsoft.VisualStudio.TestPlatform.UnitTestFramework; +using Newtonsoft.Json.Linq; +using ReactNative.Bridge; +using ReactNative.Modules.Storage; +using System; +using System.Threading; + +namespace ReactNative.Tests.Modules.Storage +{ + [TestClass] + public class AsyncStorageModuleTests + { + [TestMethod] + public void AsyncStorageModuleTests_ArgumentChecks() + { + var module = new AsyncStorageModule(new ReactContext()); + var array = new JArray(); + + AssertEx.Throws( + () => module.multiGet(null, null), + ex => Assert.AreEqual("keys", ex.ParamName)); + + AssertEx.Throws( + () => module.multiGet(array, null), + ex => Assert.AreEqual("callback", ex.ParamName)); + + AssertEx.Throws( + () => module.multiSet(null, null), + ex => Assert.AreEqual("keyValueArray", ex.ParamName)); + + AssertEx.Throws( + () => module.multiRemove(null, null), + ex => Assert.AreEqual("keys", ex.ParamName)); + + AssertEx.Throws( + () => module.multiMerge(null, null), + ex => Assert.AreEqual("keyValueArray", ex.ParamName)); + + AssertEx.Throws( + () => module.getAllKeys(null), + ex => Assert.AreEqual("callback", ex.ParamName)); + } + + [TestMethod] + public void AsyncStorageModule_InvalidKeyValue_Method() + { + var module = new AsyncStorageModule(new ReactContext()); + var waitHandle = new AutoResetEvent(false); + + var result = new JArray(); + + var callback = new MockCallback( res => { result = (JArray)res[0]; waitHandle.Set(); }); + + var array = new JArray { new JArray { 5, 5 } }; + + module.multiSet(array, callback); + waitHandle.WaitOne(); + + Assert.AreEqual((result[0]).First.ToObject(), "Invalid key"); + + array = new JArray { new JArray { 5 } }; + + module.multiSet(array, callback); + waitHandle.WaitOne(); + + Assert.AreEqual((result[0]).First.ToObject(), "Invalid key value pair"); + + array = new JArray { new JArray { 5, 5, 5 } }; + + module.multiSet(array, callback); + waitHandle.WaitOne(); + + Assert.AreEqual((result[0]).First.ToObject(), "Invalid key value pair"); + } + + [TestMethod] + public void AsyncStorageModule_multiGet_Method() + { + var module = new AsyncStorageModule(new ReactContext()); + var waitHandle = new AutoResetEvent(false); + + var result = new JArray(); + + var emptyCallback = new MockCallback(_ => waitHandle.Set()); + var callback = new MockCallback(res => { result = (JArray)res[0]; waitHandle.Set(); }); + + var array = new JArray { new JArray { "test1", 5 } }; + + module.multiSet(array, callback); + waitHandle.WaitOne(); + + module.multiGet(new JArray { "test1" }, callback); + waitHandle.WaitOne(); + + Assert.AreEqual(result.Count, 1); + Assert.AreEqual((result[0]).Last.ToObject(), 5); + } + + [TestMethod] + public void AsyncStorageModule_multiMerge_Method() + { + var module = new AsyncStorageModule(new ReactContext()); + var waitHandle = new AutoResetEvent(false); + + var result = new JArray(); + + var emptyCallback = new MockCallback(_ => waitHandle.Set()); + var callback = new MockCallback(res => { result = (JArray)res[0]; waitHandle.Set(); }); + + var array1 = new JArray { new JArray { "test1", 5 }, new JArray { "test2", 10 } }; + var array2 = new JArray { new JArray { "test2", 15 }, new JArray { "test3", 20 } }; + + module.clear(emptyCallback); + waitHandle.WaitOne(); + + module.multiSet(array1, callback); + waitHandle.WaitOne(); + + module.multiMerge(array2, callback); + waitHandle.WaitOne(); + + module.getAllKeys(callback); + waitHandle.WaitOne(); + + Assert.AreEqual(result.Count, 3); + } + + [TestMethod] + public void AsyncStorageModule_multiRemove_Method() + { + var module = new AsyncStorageModule(new ReactContext()); + var waitHandle = new AutoResetEvent(false); + + var result = new JArray(); + + var emptyCallback = new MockCallback(_ => waitHandle.Set()); + var callback = new MockCallback(res => { result = (JArray)res[0]; waitHandle.Set(); }); + + var array = new JArray { new JArray { "test1", 5 }, new JArray { "test2", 10.5 }, new JArray { "test3", new JArray { 20, 30, 40 } }, new JArray { "test4", true }, new JArray { "test5", "ABCDEF" }, new JArray { "test6", JValue.CreateNull() } }; + + module.clear(emptyCallback); + waitHandle.WaitOne(); + + module.multiSet(array, callback); + waitHandle.WaitOne(); + + module.getAllKeys(callback); + waitHandle.WaitOne(); + + Assert.AreEqual(result.Count, 6); + + module.multiGet(result, callback); + waitHandle.WaitOne(); + + // testing that result contains all values from array + foreach (var item in array) + { + string key = item.First.ToObject(); + object value = item.Last.ToObject(); + + var found = false; + var item_ = result.First; + + while (!found && item_ != null) + { + if ((item_.First).ToObject().CompareTo(key) == 0) + { + object o = item_.Last.ToObject(); + if (value == null) Assert.IsNull(o); + else if (o.GetType() != value.GetType()) Assert.Fail(); + else + { + if (value.GetType() == typeof(bool)) Assert.AreEqual((bool)value, (bool)o); + else if (value.GetType() == typeof(long)) Assert.AreEqual((long)value, (long)o); + else if (value.GetType() == typeof(double)) Assert.AreEqual((double)value, (double)o); + else if (value.GetType() == typeof(string)) Assert.IsTrue(((string)value).CompareTo((string)o) == 0); + } + found = true; + } + item_ = item_.Next; + } + Assert.IsTrue(found); + } + + var keys = new JArray { "test1", "test2" }; + module.multiRemove(keys, callback); + waitHandle.WaitOne(); + + module.getAllKeys(callback); + waitHandle.WaitOne(); + + Assert.AreEqual(result.Count, 4); + } + } +} diff --git a/ReactNative.Tests/ReactNative.Tests.csproj b/ReactNative.Tests/ReactNative.Tests.csproj index 093c758cf0a..f671724536d 100644 --- a/ReactNative.Tests/ReactNative.Tests.csproj +++ b/ReactNative.Tests/ReactNative.Tests.csproj @@ -123,6 +123,7 @@ + diff --git a/ReactNative/Modules/Storage/AsyncStorageModule.cs b/ReactNative/Modules/Storage/AsyncStorageModule.cs index 8d181520deb..ad11ca49e62 100644 --- a/ReactNative/Modules/Storage/AsyncStorageModule.cs +++ b/ReactNative/Modules/Storage/AsyncStorageModule.cs @@ -1,15 +1,34 @@ -using ReactNative.Bridge; +using Newtonsoft.Json.Linq; +using ReactNative.Bridge; +using System; +using Windows.Storage; namespace ReactNative.Modules.Storage { /// /// The asynchronous storage module. /// - /// - /// TODO: implement. - /// - public class AsyncStorageModule : NativeModuleBase + public class AsyncStorageModule : ReactContextNativeModuleBase { + private readonly string _arr = "A"; + private readonly string _nul = "N"; + private readonly string _str = "S"; + + private readonly string _invalidKey = "Invalid key"; + private readonly string _invalidPair = "Invalid key value pair"; + + private ApplicationDataContainer _container; + + /// + /// Instantiates the . + /// + /// The context. + internal AsyncStorageModule(ReactContext reactContext) + : base(reactContext) + { + _container = ApplicationData.Current.LocalSettings.CreateContainer(Name, ApplicationDataCreateDisposition.Always); + } + /// /// The name of the module. /// @@ -20,5 +39,240 @@ public override string Name return "AsyncLocalStorage"; } } + + /// + /// Given an array of keys, this returns a map of (key, value) pairs for the keys found, and + /// (key, null) for the keys that haven't been found. + /// + /// Array of key values + /// Callback. + [ReactMethod] + public void multiGet(JArray keys, ICallback callback) + { + if (keys == null) + throw new ArgumentNullException(nameof(keys)); + + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + var result = new JArray(); + + foreach (var key in keys) + { + if (key.Type == JTokenType.String) + { + var value = _container.Values[key.ToObject()]; + if (value == null) + { + result.Add(new JArray { key, JValue.CreateNull() }); + } + else + { + if (value.GetType() == typeof(bool) || value.GetType() == typeof(long) || value.GetType() == typeof(double)) + { + result.Add(new JArray { key, JToken.FromObject(value) }); + } + else if (value.GetType() == typeof(string)) + { + if ( ((string)value).StartsWith(_arr) ) + { + result.Add(new JArray { key, JToken.Parse(((string)value).Substring(1)) }); + } + else if (((string)value).StartsWith(_str)) + { + result.Add(new JArray { key, ((string)value).Substring(1) }); + } + else result.Add(new JArray { key, JValue.CreateNull() }); + } + } + } + } + callback.Invoke(result); + } + + /// + /// Inserts multiple (key, value) pairs. The insertion will replace conflicting (key, value) pairs. + /// + /// Array of key values + /// Callback. + [ReactMethod] + public void multiSet(JArray keyValueArray, ICallback callback) + { + if (keyValueArray == null) + throw new ArgumentNullException(nameof(keyValueArray)); + + var result = new JArray(); + + foreach (var keyValue in keyValueArray) + { + if (keyValue.Type == JTokenType.Array && keyValue.ToObject().Count == 2) + { + var pair = keyValue.ToObject(); + if (pair.First.Type == JTokenType.String) + { + var key = pair.First.ToObject(); + switch (pair.Last.Type) + { + case JTokenType.Null: + _container.Values[key] = _nul; + break; + case JTokenType.Boolean: + _container.Values[key] = pair.Last.ToObject(); + break; + case JTokenType.Integer: + _container.Values[key] = pair.Last.ToObject(); + break; + case JTokenType.Float: + _container.Values[key] = pair.Last.ToObject(); + break; + case JTokenType.String: + _container.Values[key] = _str + pair.Last.ToObject(); + break; + case JTokenType.Array: + _container.Values[key] = _arr + pair.Last.ToString(); + break; + default: + break; + } + } + else + { + result.Add(new JArray { _invalidKey, pair.First }); + } + } + else + { + result.Add(new JArray { _invalidPair, keyValue }); + } + } + callback?.Invoke(result); + } + + /// + /// Removes all rows of the keys given. + /// + /// Array of key values + /// Callback. + [ReactMethod] + public void multiRemove(JArray keys, ICallback callback) + { + var result = new JArray(); + if (keys == null) + throw new ArgumentNullException(nameof(keys)); + + foreach (var key in keys) + { + if (key.Type == JTokenType.String) + { + _container.Values.Remove(key.ToObject()); + } + else + { + result.Add(new JArray { _invalidKey, key }); + } + } + callback?.Invoke(result); + } + + /// + /// Given an array of (key, value) pairs, this will merge the given values with the stored values + /// of the given keys, if they exist. + /// + /// Array of key values + /// Callback. + [ReactMethod] + public void multiMerge(JArray keyValueArray, ICallback callback) + { + if (keyValueArray == null) + throw new ArgumentNullException(nameof(keyValueArray)); + + var result = new JArray(); + + foreach (var keyValue in keyValueArray) + { + if (keyValue.Type == JTokenType.Array && keyValue.ToObject().Count == 2) + { + var pair = keyValue.ToObject(); + if (pair.First.Type == JTokenType.String) + { + var key = pair.First.ToObject(); + var value = _container.Values[key]; + var token = pair.Last; + if (value != null) + { + JToken tokenOld = JValue.CreateNull(); + if (value.GetType() == typeof(bool) || value.GetType() == typeof(long) || value.GetType() == typeof(double)) + { + tokenOld = JToken.FromObject(value); + } + else if (value.GetType() == typeof(string) && ((string)value).Length > 0) + { + if (((string)value).StartsWith(_arr)) + { + tokenOld = JToken.Parse(((string)value).Substring(1)); + } + else if (((string)value).StartsWith(_str)) + { + tokenOld = JToken.FromObject(((string)value).Substring(1)); + } + } + if (tokenOld.Type == JTokenType.Array) + { + ((JArray)tokenOld).Merge(token); + token = tokenOld; + } + else if (token.Type == JTokenType.Array) + { + ((JArray)token).Merge(tokenOld); + } + } + if (token.Type != JTokenType.Null) + { + _container.Values[key] = pair.Last.ToString(); + } + } + else + { + result.Add(new JArray { _invalidKey, pair.First }); + } + } + else + { + result.Add(new JArray { _invalidPair, keyValue }); + } + } + callback?.Invoke(result); + } + + /// + /// Clears the database. + /// + /// Callback. + [ReactMethod] + public void clear(ICallback callback) + { + ApplicationData.Current.LocalSettings.DeleteContainer(Name); + _container = ApplicationData.Current.LocalSettings.CreateContainer(Name, ApplicationDataCreateDisposition.Always); + callback?.Invoke(); + } + + /// + /// Returns an array with all keys from the database. + /// + /// Callback. + [ReactMethod] + public void getAllKeys(ICallback callback) + { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + var result = new JArray(); + var keys = _container.Values.Keys; + foreach (var key in keys) + { + result.Add(JToken.FromObject(key)); + } + callback.Invoke(result); + } } }