[ReactNative] Implement merge functionality for AsyncStorage

This commit is contained in:
Spencer Ahrens 2015-06-03 16:57:08 -07:00
parent 9fe7128493
commit 7ffa7bd1f1
4 changed files with 117 additions and 19 deletions

View File

@ -16,12 +16,19 @@ var {
View, View,
} = React; } = React;
var deepDiffer = require('deepDiffer');
var DEBUG = false; var DEBUG = false;
var KEY_1 = 'key_1'; var KEY_1 = 'key_1';
var VAL_1 = 'val_1'; var VAL_1 = 'val_1';
var KEY_2 = 'key_2'; var KEY_2 = 'key_2';
var VAL_2 = 'val_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 // setup in componentDidMount
var done; var done;
@ -40,8 +47,9 @@ function expectTrue(condition, message) {
function expectEqual(lhs, rhs, testname) { function expectEqual(lhs, rhs, testname) {
expectTrue( expectTrue(
lhs === rhs, !deepDiffer(lhs, rhs),
'Error in test ' + testname + ': expected ' + rhs + ', got ' + lhs '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 + ')' 'Missing KEY_1 or KEY_2 in ' + '(' + result + ')'
); );
updateMessage('testRemoveItem - add two items'); updateMessage('testRemoveItem - add two items');
AsyncStorage.removeItem(KEY_1, (err) => { AsyncStorage.removeItem(KEY_1, (err2) => {
expectAsyncNoError(err); expectAsyncNoError(err2);
updateMessage('delete successful '); updateMessage('delete successful ');
AsyncStorage.getItem(KEY_1, (err, result) => { AsyncStorage.getItem(KEY_1, (err3, result2) => {
expectAsyncNoError(err); expectAsyncNoError(err3);
expectEqual( expectEqual(
result, result2,
null, null,
'testRemoveItem: key_1 present after delete' 'testRemoveItem: key_1 present after delete'
); );
updateMessage('key properly removed '); updateMessage('key properly removed ');
AsyncStorage.getAllKeys((err, result2) => { AsyncStorage.getAllKeys((err4, result3) => {
expectAsyncNoError(err); expectAsyncNoError(err4);
expectTrue( expectTrue(
result2.indexOf(KEY_1) === -1, result3.indexOf(KEY_1) === -1,
'Unexpected: KEY_1 present in ' + result2 'Unexpected: KEY_1 present in ' + result3
); );
updateMessage('proper length returned.\nDone!'); updateMessage('proper length returned.');
done(); 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({ var AsyncStorageTest = React.createClass({
getInitialState() { getInitialState() {
return { return {

View File

@ -18,6 +18,8 @@
// Utility functions for JSON object <-> string serialization/deserialization // Utility functions for JSON object <-> string serialization/deserialization
RCT_EXTERN NSString *RCTJSONStringify(id jsonObject, NSError **error); RCT_EXTERN NSString *RCTJSONStringify(id jsonObject, NSError **error);
RCT_EXTERN id RCTJSONParse(NSString *jsonString, 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 // Strip non JSON-safe values from an object graph
RCT_EXTERN id RCTJSONClean(id object); RCT_EXTERN id RCTJSONClean(id object);

View File

@ -24,7 +24,7 @@ NSString *RCTJSONStringify(id jsonObject, NSError **error)
return jsonData ? [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding] : nil; 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) { if (!jsonString) {
return nil; return nil;
@ -39,7 +39,15 @@ id RCTJSONParse(NSString *jsonString, NSError **error)
return nil; 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) id RCTJSONClean(id object)

View File

@ -61,6 +61,34 @@ static id RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut
return nil; 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 #pragma mark - RCTAsyncLocalStorage
@implementation RCTAsyncLocalStorage @implementation RCTAsyncLocalStorage
@ -135,13 +163,19 @@ RCT_EXPORT_MODULE()
if (errorOut) { if (errorOut) {
return 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. id value = _manifest[key]; // nil means missing, null means there is a data file, anything else is an inline value.
if (value == [NSNull null]) { if (value == [NSNull null]) {
NSString *filePath = [self _filePathForKey:key]; 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 value;
return errorOut;
} }
- (id)_writeEntry:(NSArray *)entry - (id)_writeEntry:(NSArray *)entry
@ -198,7 +232,6 @@ RCT_EXPORT_METHOD(multiGet:(NSArray *)keys
id keyError = [self _appendItemForKey:key toArray:result]; id keyError = [self _appendItemForKey:key toArray:result];
RCTAppendError(keyError, &errors); RCTAppendError(keyError, &errors);
} }
[self _writeManifest:&errors];
callback(@[errors ?: [NSNull null], result]); 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 RCT_EXPORT_METHOD(multiRemove:(NSArray *)keys
callback:(RCTResponseSenderBlock)callback) callback:(RCTResponseSenderBlock)callback)
{ {