[ReactNative] Implement merge functionality for AsyncStorage
This commit is contained in:
parent
9fe7128493
commit
7ffa7bd1f1
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue