Skip to content

Commit 7ffa7bd

Browse files
committed
[ReactNative] Implement merge functionality for AsyncStorage
1 parent 9fe7128 commit 7ffa7bd

4 files changed

Lines changed: 117 additions & 19 deletions

File tree

IntegrationTests/AsyncStorageTest.js

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,19 @@ var {
1616
View,
1717
} = React;
1818

19+
var deepDiffer = require('deepDiffer');
20+
1921
var DEBUG = false;
2022

2123
var KEY_1 = 'key_1';
2224
var VAL_1 = 'val_1';
2325
var KEY_2 = 'key_2';
2426
var VAL_2 = 'val_2';
27+
var KEY_MERGE = 'key_merge';
28+
var VAL_MERGE_1 = {'foo': 1, 'bar': {'hoo': 1, 'boo': 1}, 'moo': {'a': 3}};
29+
var VAL_MERGE_2 = {'bar': {'hoo': 2}, 'baz': 2, 'moo': {'a': 3}};
30+
var VAL_MERGE_EXPECT =
31+
{'foo': 1, 'bar': {'hoo': 2, 'boo': 1}, 'baz': 2, 'moo': {'a': 3}};
2532

2633
// setup in componentDidMount
2734
var done;
@@ -40,8 +47,9 @@ function expectTrue(condition, message) {
4047

4148
function expectEqual(lhs, rhs, testname) {
4249
expectTrue(
43-
lhs === rhs,
44-
'Error in test ' + testname + ': expected ' + rhs + ', got ' + lhs
50+
!deepDiffer(lhs, rhs),
51+
'Error in test ' + testname + ': expected\n' + JSON.stringify(rhs) +
52+
'\ngot\n' + JSON.stringify(lhs)
4553
);
4654
}
4755

@@ -93,25 +101,25 @@ function testRemoveItem() {
93101
'Missing KEY_1 or KEY_2 in ' + '(' + result + ')'
94102
);
95103
updateMessage('testRemoveItem - add two items');
96-
AsyncStorage.removeItem(KEY_1, (err) => {
97-
expectAsyncNoError(err);
104+
AsyncStorage.removeItem(KEY_1, (err2) => {
105+
expectAsyncNoError(err2);
98106
updateMessage('delete successful ');
99-
AsyncStorage.getItem(KEY_1, (err, result) => {
100-
expectAsyncNoError(err);
107+
AsyncStorage.getItem(KEY_1, (err3, result2) => {
108+
expectAsyncNoError(err3);
101109
expectEqual(
102-
result,
110+
result2,
103111
null,
104112
'testRemoveItem: key_1 present after delete'
105113
);
106114
updateMessage('key properly removed ');
107-
AsyncStorage.getAllKeys((err, result2) => {
108-
expectAsyncNoError(err);
115+
AsyncStorage.getAllKeys((err4, result3) => {
116+
expectAsyncNoError(err4);
109117
expectTrue(
110-
result2.indexOf(KEY_1) === -1,
111-
'Unexpected: KEY_1 present in ' + result2
118+
result3.indexOf(KEY_1) === -1,
119+
'Unexpected: KEY_1 present in ' + result3
112120
);
113-
updateMessage('proper length returned.\nDone!');
114-
done();
121+
updateMessage('proper length returned.');
122+
runTestCase('should merge values', testMerge);
115123
});
116124
});
117125
});
@@ -120,6 +128,21 @@ function testRemoveItem() {
120128
});
121129
}
122130

131+
function testMerge() {
132+
AsyncStorage.setItem(KEY_MERGE, JSON.stringify(VAL_MERGE_1), (err1) => {
133+
expectAsyncNoError(err1);
134+
AsyncStorage.mergeItem(KEY_MERGE, JSON.stringify(VAL_MERGE_2), (err2) => {
135+
expectAsyncNoError(err2);
136+
AsyncStorage.getItem(KEY_MERGE, (err3, result) => {
137+
expectAsyncNoError(err3);
138+
expectEqual(JSON.parse(result), VAL_MERGE_EXPECT, 'testMerge');
139+
updateMessage('objects deeply merged\nDone!');
140+
done();
141+
});
142+
});
143+
});
144+
}
145+
123146
var AsyncStorageTest = React.createClass({
124147
getInitialState() {
125148
return {

React/Base/RCTUtils.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
// Utility functions for JSON object <-> string serialization/deserialization
1919
RCT_EXTERN NSString *RCTJSONStringify(id jsonObject, NSError **error);
2020
RCT_EXTERN id RCTJSONParse(NSString *jsonString, NSError **error);
21+
RCT_EXTERN id RCTJSONParseMutable(NSString *jsonString, NSError **error);
22+
RCT_EXTERN id RCTJSONParseWithOptions(NSString *jsonString, NSError **error, NSJSONReadingOptions options);
2123

2224
// Strip non JSON-safe values from an object graph
2325
RCT_EXTERN id RCTJSONClean(id object);

React/Base/RCTUtils.m

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
return jsonData ? [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding] : nil;
2525
}
2626

27-
id RCTJSONParse(NSString *jsonString, NSError **error)
27+
id RCTJSONParseWithOptions(NSString *jsonString, NSError **error, NSJSONReadingOptions options)
2828
{
2929
if (!jsonString) {
3030
return nil;
@@ -39,7 +39,15 @@ id RCTJSONParse(NSString *jsonString, NSError **error)
3939
return nil;
4040
}
4141
}
42-
return [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingAllowFragments error:error];
42+
return [NSJSONSerialization JSONObjectWithData:jsonData options:options error:error];
43+
}
44+
45+
id RCTJSONParse(NSString *jsonString, NSError **error) {
46+
return RCTJSONParseWithOptions(jsonString, error, NSJSONReadingAllowFragments);
47+
}
48+
49+
id RCTJSONParseMutable(NSString *jsonString, NSError **error) {
50+
return RCTJSONParseWithOptions(jsonString, error, NSJSONReadingMutableContainers|NSJSONReadingMutableLeaves);
4351
}
4452

4553
id RCTJSONClean(id object)

React/Modules/RCTAsyncLocalStorage.m

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,34 @@ static id RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut
6161
return nil;
6262
}
6363

64+
// Only merges objects - all other types are just clobbered (including arrays)
65+
static void RCTMergeRecursive(NSMutableDictionary *destination, NSDictionary *source)
66+
{
67+
for (NSString *key in source) {
68+
id sourceValue = source[key];
69+
if ([sourceValue isKindOfClass:[NSDictionary class]]) {
70+
id destinationValue = destination[key];
71+
NSMutableDictionary *nestedDestination;
72+
if ([destinationValue classForCoder] == [NSMutableDictionary class]) {
73+
nestedDestination = destinationValue;
74+
} else {
75+
if ([destinationValue isKindOfClass:[NSDictionary class]]) {
76+
// Ideally we wouldn't eagerly copy here...
77+
nestedDestination = [destinationValue mutableCopy];
78+
} else {
79+
destination[key] = [sourceValue copy];
80+
}
81+
}
82+
if (nestedDestination) {
83+
RCTMergeRecursive(nestedDestination, sourceValue);
84+
destination[key] = nestedDestination;
85+
}
86+
} else {
87+
destination[key] = sourceValue;
88+
}
89+
}
90+
}
91+
6492
#pragma mark - RCTAsyncLocalStorage
6593

6694
@implementation RCTAsyncLocalStorage
@@ -135,13 +163,19 @@ - (id)_appendItemForKey:(NSString *)key toArray:(NSMutableArray *)result
135163
if (errorOut) {
136164
return errorOut;
137165
}
166+
id value = [self _getValueForKey:key errorOut:&errorOut];
167+
[result addObject:@[key, value ?: [NSNull null]]]; // Insert null if missing or failure.
168+
return errorOut;
169+
}
170+
171+
- (NSString *)_getValueForKey:(NSString *)key errorOut:(NSDictionary **)errorOut
172+
{
138173
id value = _manifest[key]; // nil means missing, null means there is a data file, anything else is an inline value.
139174
if (value == [NSNull null]) {
140175
NSString *filePath = [self _filePathForKey:key];
141-
value = RCTReadFile(filePath, key, &errorOut);
176+
value = RCTReadFile(filePath, key, errorOut);
142177
}
143-
[result addObject:@[key, value ?: [NSNull null]]]; // Insert null if missing or failure.
144-
return errorOut;
178+
return value;
145179
}
146180

147181
- (id)_writeEntry:(NSArray *)entry
@@ -198,7 +232,6 @@ - (id)_writeEntry:(NSArray *)entry
198232
id keyError = [self _appendItemForKey:key toArray:result];
199233
RCTAppendError(keyError, &errors);
200234
}
201-
[self _writeManifest:&errors];
202235
callback(@[errors ?: [NSNull null], result]);
203236
}
204237

@@ -221,6 +254,38 @@ - (id)_writeEntry:(NSArray *)entry
221254
}
222255
}
223256

257+
RCT_EXPORT_METHOD(multiMerge:(NSArray *)kvPairs
258+
callback:(RCTResponseSenderBlock)callback)
259+
{
260+
id errorOut = [self _ensureSetup];
261+
if (errorOut) {
262+
callback(@[@[errorOut]]);
263+
return;
264+
}
265+
NSMutableArray *errors;
266+
for (__strong NSArray *entry in kvPairs) {
267+
id keyError;
268+
NSString *value = [self _getValueForKey:entry[0] errorOut:&keyError];
269+
if (keyError) {
270+
RCTAppendError(keyError, &errors);
271+
} else {
272+
if (value) {
273+
NSMutableDictionary *mergedVal = [RCTJSONParseMutable(value, &keyError) mutableCopy];
274+
RCTMergeRecursive(mergedVal, RCTJSONParse(entry[1], &keyError));
275+
entry = @[entry[0], RCTJSONStringify(mergedVal, &keyError)];
276+
}
277+
if (!keyError) {
278+
keyError = [self _writeEntry:entry];
279+
}
280+
RCTAppendError(keyError, &errors);
281+
}
282+
}
283+
[self _writeManifest:&errors];
284+
if (callback) {
285+
callback(@[errors ?: [NSNull null]]);
286+
}
287+
}
288+
224289
RCT_EXPORT_METHOD(multiRemove:(NSArray *)keys
225290
callback:(RCTResponseSenderBlock)callback)
226291
{

0 commit comments

Comments
 (0)