From 7ffa7bd1f101166fce2cc8016df405b2fb67d4aa Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Wed, 3 Jun 2015 16:57:08 -0700 Subject: [PATCH] [ReactNative] Implement merge functionality for AsyncStorage --- IntegrationTests/AsyncStorageTest.js | 49 ++++++++++++++----- React/Base/RCTUtils.h | 2 + React/Base/RCTUtils.m | 12 ++++- React/Modules/RCTAsyncLocalStorage.m | 73 ++++++++++++++++++++++++++-- 4 files changed, 117 insertions(+), 19 deletions(-) diff --git a/IntegrationTests/AsyncStorageTest.js b/IntegrationTests/AsyncStorageTest.js index 6d13bb6e9..911887d3e 100644 --- a/IntegrationTests/AsyncStorageTest.js +++ b/IntegrationTests/AsyncStorageTest.js @@ -16,12 +16,19 @@ var { View, } = React; +var deepDiffer = require('deepDiffer'); + var DEBUG = false; var KEY_1 = 'key_1'; var VAL_1 = 'val_1'; var KEY_2 = 'key_2'; var VAL_2 = 'val_2'; +var KEY_MERGE = 'key_merge'; +var VAL_MERGE_1 = {'foo': 1, 'bar': {'hoo': 1, 'boo': 1}, 'moo': {'a': 3}}; +var VAL_MERGE_2 = {'bar': {'hoo': 2}, 'baz': 2, 'moo': {'a': 3}}; +var VAL_MERGE_EXPECT = + {'foo': 1, 'bar': {'hoo': 2, 'boo': 1}, 'baz': 2, 'moo': {'a': 3}}; // setup in componentDidMount var done; @@ -40,8 +47,9 @@ function expectTrue(condition, message) { function expectEqual(lhs, rhs, testname) { expectTrue( - lhs === rhs, - 'Error in test ' + testname + ': expected ' + rhs + ', got ' + lhs + !deepDiffer(lhs, rhs), + 'Error in test ' + testname + ': expected\n' + JSON.stringify(rhs) + + '\ngot\n' + JSON.stringify(lhs) ); } @@ -93,25 +101,25 @@ function testRemoveItem() { 'Missing KEY_1 or KEY_2 in ' + '(' + result + ')' ); updateMessage('testRemoveItem - add two items'); - AsyncStorage.removeItem(KEY_1, (err) => { - expectAsyncNoError(err); + AsyncStorage.removeItem(KEY_1, (err2) => { + expectAsyncNoError(err2); updateMessage('delete successful '); - AsyncStorage.getItem(KEY_1, (err, result) => { - expectAsyncNoError(err); + AsyncStorage.getItem(KEY_1, (err3, result2) => { + expectAsyncNoError(err3); expectEqual( - result, + result2, null, 'testRemoveItem: key_1 present after delete' ); updateMessage('key properly removed '); - AsyncStorage.getAllKeys((err, result2) => { - expectAsyncNoError(err); + AsyncStorage.getAllKeys((err4, result3) => { + expectAsyncNoError(err4); expectTrue( - result2.indexOf(KEY_1) === -1, - 'Unexpected: KEY_1 present in ' + result2 + result3.indexOf(KEY_1) === -1, + 'Unexpected: KEY_1 present in ' + result3 ); - updateMessage('proper length returned.\nDone!'); - done(); + updateMessage('proper length returned.'); + runTestCase('should merge values', testMerge); }); }); }); @@ -120,6 +128,21 @@ function testRemoveItem() { }); } +function testMerge() { + AsyncStorage.setItem(KEY_MERGE, JSON.stringify(VAL_MERGE_1), (err1) => { + expectAsyncNoError(err1); + AsyncStorage.mergeItem(KEY_MERGE, JSON.stringify(VAL_MERGE_2), (err2) => { + expectAsyncNoError(err2); + AsyncStorage.getItem(KEY_MERGE, (err3, result) => { + expectAsyncNoError(err3); + expectEqual(JSON.parse(result), VAL_MERGE_EXPECT, 'testMerge'); + updateMessage('objects deeply merged\nDone!'); + done(); + }); + }); + }); +} + var AsyncStorageTest = React.createClass({ getInitialState() { return { diff --git a/React/Base/RCTUtils.h b/React/Base/RCTUtils.h index 5c34d0e0a..641500b38 100644 --- a/React/Base/RCTUtils.h +++ b/React/Base/RCTUtils.h @@ -18,6 +18,8 @@ // Utility functions for JSON object <-> string serialization/deserialization RCT_EXTERN NSString *RCTJSONStringify(id jsonObject, NSError **error); RCT_EXTERN id RCTJSONParse(NSString *jsonString, NSError **error); +RCT_EXTERN id RCTJSONParseMutable(NSString *jsonString, NSError **error); +RCT_EXTERN id RCTJSONParseWithOptions(NSString *jsonString, NSError **error, NSJSONReadingOptions options); // Strip non JSON-safe values from an object graph RCT_EXTERN id RCTJSONClean(id object); diff --git a/React/Base/RCTUtils.m b/React/Base/RCTUtils.m index 712e9724e..613b13163 100644 --- a/React/Base/RCTUtils.m +++ b/React/Base/RCTUtils.m @@ -24,7 +24,7 @@ NSString *RCTJSONStringify(id jsonObject, NSError **error) return jsonData ? [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding] : nil; } -id RCTJSONParse(NSString *jsonString, NSError **error) +id RCTJSONParseWithOptions(NSString *jsonString, NSError **error, NSJSONReadingOptions options) { if (!jsonString) { return nil; @@ -39,7 +39,15 @@ id RCTJSONParse(NSString *jsonString, NSError **error) return nil; } } - return [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingAllowFragments error:error]; + return [NSJSONSerialization JSONObjectWithData:jsonData options:options error:error]; +} + +id RCTJSONParse(NSString *jsonString, NSError **error) { + return RCTJSONParseWithOptions(jsonString, error, NSJSONReadingAllowFragments); +} + +id RCTJSONParseMutable(NSString *jsonString, NSError **error) { + return RCTJSONParseWithOptions(jsonString, error, NSJSONReadingMutableContainers|NSJSONReadingMutableLeaves); } id RCTJSONClean(id object) diff --git a/React/Modules/RCTAsyncLocalStorage.m b/React/Modules/RCTAsyncLocalStorage.m index 2c01161d4..76f7fa885 100644 --- a/React/Modules/RCTAsyncLocalStorage.m +++ b/React/Modules/RCTAsyncLocalStorage.m @@ -61,6 +61,34 @@ static id RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut return nil; } +// Only merges objects - all other types are just clobbered (including arrays) +static void RCTMergeRecursive(NSMutableDictionary *destination, NSDictionary *source) +{ + for (NSString *key in source) { + id sourceValue = source[key]; + if ([sourceValue isKindOfClass:[NSDictionary class]]) { + id destinationValue = destination[key]; + NSMutableDictionary *nestedDestination; + if ([destinationValue classForCoder] == [NSMutableDictionary class]) { + nestedDestination = destinationValue; + } else { + if ([destinationValue isKindOfClass:[NSDictionary class]]) { + // Ideally we wouldn't eagerly copy here... + nestedDestination = [destinationValue mutableCopy]; + } else { + destination[key] = [sourceValue copy]; + } + } + if (nestedDestination) { + RCTMergeRecursive(nestedDestination, sourceValue); + destination[key] = nestedDestination; + } + } else { + destination[key] = sourceValue; + } + } +} + #pragma mark - RCTAsyncLocalStorage @implementation RCTAsyncLocalStorage @@ -135,13 +163,19 @@ RCT_EXPORT_MODULE() if (errorOut) { return errorOut; } + id value = [self _getValueForKey:key errorOut:&errorOut]; + [result addObject:@[key, value ?: [NSNull null]]]; // Insert null if missing or failure. + return errorOut; +} + +- (NSString *)_getValueForKey:(NSString *)key errorOut:(NSDictionary **)errorOut +{ id value = _manifest[key]; // nil means missing, null means there is a data file, anything else is an inline value. if (value == [NSNull null]) { NSString *filePath = [self _filePathForKey:key]; - value = RCTReadFile(filePath, key, &errorOut); + value = RCTReadFile(filePath, key, errorOut); } - [result addObject:@[key, value ?: [NSNull null]]]; // Insert null if missing or failure. - return errorOut; + return value; } - (id)_writeEntry:(NSArray *)entry @@ -198,7 +232,6 @@ RCT_EXPORT_METHOD(multiGet:(NSArray *)keys id keyError = [self _appendItemForKey:key toArray:result]; RCTAppendError(keyError, &errors); } - [self _writeManifest:&errors]; callback(@[errors ?: [NSNull null], result]); } @@ -221,6 +254,38 @@ RCT_EXPORT_METHOD(multiSet:(NSArray *)kvPairs } } +RCT_EXPORT_METHOD(multiMerge:(NSArray *)kvPairs + callback:(RCTResponseSenderBlock)callback) +{ + id errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[@[errorOut]]); + return; + } + NSMutableArray *errors; + for (__strong NSArray *entry in kvPairs) { + id keyError; + NSString *value = [self _getValueForKey:entry[0] errorOut:&keyError]; + if (keyError) { + RCTAppendError(keyError, &errors); + } else { + if (value) { + NSMutableDictionary *mergedVal = [RCTJSONParseMutable(value, &keyError) mutableCopy]; + RCTMergeRecursive(mergedVal, RCTJSONParse(entry[1], &keyError)); + entry = @[entry[0], RCTJSONStringify(mergedVal, &keyError)]; + } + if (!keyError) { + keyError = [self _writeEntry:entry]; + } + RCTAppendError(keyError, &errors); + } + } + [self _writeManifest:&errors]; + if (callback) { + callback(@[errors ?: [NSNull null]]); + } +} + RCT_EXPORT_METHOD(multiRemove:(NSArray *)keys callback:(RCTResponseSenderBlock)callback) {