@samitouri / QOS-React-2 / commits / ab2d59f8b0

[addons] update() immutability helper

Dealing with immutable data is hard. This provides a simple helper (ported from the IG codebase) that makes dealing with immutable JSON data easier. Designed to be familiar for people who use MongoDB.

Pete Hunt committed Feb 24, 2014 at 13:57 UTC ab2d59f8b02dfe65d2fe11960389671816904a06
2 files changed +254
src/addons/__tests__/update-test.js new
+95
@@ -0,0 +1,95 @@
1 +/**
2 + * Copyright 2013-2014 Facebook, Inc.
3 + *
4 + * Licensed under the Apache License, Version 2.0 (the "License");
5 + * you may not use this file except in compliance with the License.
6 + * You may obtain a copy of the License at
7 + *
8 + * http://www.apache.org/licenses/LICENSE-2.0
9 + *
10 + * Unless required by applicable law or agreed to in writing, software
11 + * distributed under the License is distributed on an "AS IS" BASIS,
12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 + * See the License for the specific language governing permissions and
14 + * limitations under the License.
15 + *
16 + * @emails react-core
17 + */
18 +
19 +"use strict";
20 +
21 +var update = require('update');
22 +
23 +describe('update', function() {
24 + it('should support push', function() {
25 + expect(update([1], {$push: [7]})).toEqual([1, 7]);
26 + expect(update.bind(null, [], {$push: 7})).toThrow(
27 + 'Invariant Violation: update(): expected spec of $push to be an ' +
28 + 'array; got 7. Did you forget to wrap your parameter in an array?'
29 + );
30 + expect(update.bind(null, 1, {$push: 7})).toThrow(
31 + 'Invariant Violation: update(): expected target of $push to be an ' +
32 + 'array; got 1.'
33 + );
34 + });
35 +
36 + it('should support unshift', function() {
37 + expect(update([1], {$unshift: [7]})).toEqual([7, 1]);
38 + expect(update.bind(null, [], {$unshift: 7})).toThrow(
39 + 'Invariant Violation: update(): expected spec of $unshift to be an ' +
40 + 'array; got 7. Did you forget to wrap your parameter in an array?'
41 + );
42 + expect(update.bind(null, 1, {$unshift: 7})).toThrow(
43 + 'Invariant Violation: update(): expected target of $unshift to be an ' +
44 + 'array; got 1.'
45 + );
46 + });
47 +
48 + it('should support splice', function() {
49 + expect(update([1, 4, 3], {$splice: [[1, 1, 2]]})).toEqual([1, 2, 3]);
50 + expect(update.bind(null, [], {$splice: 1})).toThrow(
51 + 'Invariant Violation: update(): expected spec of $splice to be an ' +
52 + 'array of arrays; got 1. Did you forget to wrap your parameters in an '+
53 + 'array?'
54 + );
55 + expect(update.bind(null, [], {$splice: [1]})).toThrow(
56 + 'Invariant Violation: update(): expected spec of $splice to be an ' +
57 + 'array of arrays; got 1. Did you forget to wrap your parameters in an ' +
58 + 'array?'
59 + );
60 + expect(update.bind(null, 1, {$splice: 7})).toThrow(
61 + 'Invariant Violation: Expected $splice target to be an array; got 1'
62 + );
63 + });
64 +
65 + it('should support merge', function() {
66 + expect(update({a: 'b'}, {$merge: {c: 'd'}})).toEqual({a: 'b', c: 'd'});
67 + expect(update.bind(null, {}, {$merge: 7})).toThrow(
68 + 'Invariant Violation: update(): $merge expects a spec of type ' +
69 + '\'object\'; got 7'
70 + );
71 + expect(update.bind(null, 7, {$merge: {a: 'b'}})).toThrow(
72 + 'Invariant Violation: update(): $merge expects a target of type ' +
73 + '\'object\'; got 7'
74 + );
75 + });
76 +
77 + it('should support set', function() {
78 + expect(update({a: 'b'}, {$set: {c: 'd'}})).toEqual({c: 'd'});
79 + });
80 +
81 + it('should support deep updates', function() {
82 + expect(update({a: 'b', c: {d: 'e'}}, {c: {d: {$set: 'f'}}})).toEqual({
83 + a: 'b',
84 + c: {d: 'f'}
85 + });
86 + });
87 +
88 + it('should require a directive', function() {
89 + expect(update.bind(null, {a: 'b'}, {a: 'c'})).toThrow(
90 + 'Invariant Violation: update(): You provided a key path to update() ' +
91 + 'that did not contain one of $push, $unshift, $splice, $set, $merge. ' +
92 + 'Did you forget to include {$set: ...}?'
93 + );
94 + });
95 +});
src/addons/update.js new
+159
@@ -0,0 +1,159 @@
1 +/**
2 + * Copyright 2013-2014 Facebook, Inc.
3 + *
4 + * Licensed under the Apache License, Version 2.0 (the "License");
5 + * you may not use this file except in compliance with the License.
6 + * You may obtain a copy of the License at
7 + *
8 + * http://www.apache.org/licenses/LICENSE-2.0
9 + *
10 + * Unless required by applicable law or agreed to in writing, software
11 + * distributed under the License is distributed on an "AS IS" BASIS,
12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 + * See the License for the specific language governing permissions and
14 + * limitations under the License.
15 + *
16 + * @providesModule update
17 + */
18 +
19 +"use strict";
20 +
21 +var copyProperties = require('copyProperties');
22 +var keyOf = require('keyOf');
23 +var invariant = require('invariant');
24 +
25 +function shallowCopy(x) {
26 + if (Array.isArray(x)) {
27 + return x.concat();
28 + } else if (x && typeof x === 'object') {
29 + return copyProperties(new x.constructor(), x);
30 + } else {
31 + return x;
32 + }
33 +}
34 +
35 +var DIRECTIVE_PUSH = keyOf({$push: null});
36 +var DIRECTIVE_UNSHIFT = keyOf({$unshift: null});
37 +var DIRECTIVE_SPLICE = keyOf({$splice: null});
38 +var DIRECTIVE_SET = keyOf({$set: null});
39 +var DIRECTIVE_MERGE = keyOf({$merge: null});
40 +
41 +var ALL_DIRECTIVES_LIST = [
42 + DIRECTIVE_PUSH,
43 + DIRECTIVE_UNSHIFT,
44 + DIRECTIVE_SPLICE,
45 + DIRECTIVE_SET,
46 + DIRECTIVE_MERGE
47 +];
48 +
49 +var ALL_DIRECTIVES_SET = {};
50 +
51 +ALL_DIRECTIVES_LIST.forEach(function(directive) {
52 + ALL_DIRECTIVES_SET[directive] = true;
53 +});
54 +
55 +function invariantArrayCase(value, spec, directive) {
56 + invariant(
57 + Array.isArray(value),
58 + 'update(): expected target of %s to be an array; got %s.',
59 + directive,
60 + value
61 + );
62 + var specValue = spec[directive];
63 + invariant(
64 + Array.isArray(specValue),
65 + 'update(): expected spec of %s to be an array; got %s. ' +
66 + 'Did you forget to wrap your parameter in an array?',
67 + directive,
68 + specValue
69 + );
70 +}
71 +
72 +function update(value, spec) {
73 + invariant(
74 + typeof spec === 'object',
75 + 'update(): You provided a key path to update() that did not contain one ' +
76 + 'of %s. Did you forget to include {%s: ...}?',
77 + ALL_DIRECTIVES_LIST.join(', '),
78 + DIRECTIVE_SET
79 + );
80 +
81 + if (spec.hasOwnProperty(DIRECTIVE_SET)) {
82 + invariant(
83 + Object.keys(spec).length === 1,
84 + 'Cannot have more than one key in an object with %s',
85 + DIRECTIVE_SET
86 + );
87 +
88 + return spec[DIRECTIVE_SET];
89 + }
90 +
91 + var nextValue = shallowCopy(value);
92 +
93 + if (spec.hasOwnProperty(DIRECTIVE_MERGE)) {
94 + var mergeObj = spec[DIRECTIVE_MERGE];
95 + invariant(
96 + mergeObj && typeof mergeObj === 'object',
97 + 'update(): %s expects a spec of type \'object\'; got %s',
98 + DIRECTIVE_MERGE,
99 + mergeObj
100 + );
101 + invariant(
102 + nextValue && typeof nextValue === 'object',
103 + 'update(): %s expects a target of type \'object\'; got %s',
104 + DIRECTIVE_MERGE,
105 + nextValue
106 + );
107 + copyProperties(nextValue, spec[DIRECTIVE_MERGE]);
108 + }
109 +
110 + if (spec.hasOwnProperty(DIRECTIVE_PUSH)) {
111 + invariantArrayCase(value, spec, DIRECTIVE_PUSH);
112 + spec[DIRECTIVE_PUSH].forEach(function(item) {
113 + nextValue.push(item);
114 + });
115 + }
116 +
117 + if (spec.hasOwnProperty(DIRECTIVE_UNSHIFT)) {
118 + invariantArrayCase(value, spec, DIRECTIVE_UNSHIFT);
119 + spec[DIRECTIVE_UNSHIFT].forEach(function(item) {
120 + nextValue.unshift(item);
121 + });
122 + }
123 +
124 + if (spec.hasOwnProperty(DIRECTIVE_SPLICE)) {
125 + invariant(
126 + Array.isArray(value),
127 + 'Expected %s target to be an array; got %s',
128 + DIRECTIVE_SPLICE,
129 + value
130 + );
131 + invariant(
132 + Array.isArray(spec[DIRECTIVE_SPLICE]),
133 + 'update(): expected spec of %s to be an array of arrays; got %s. ' +
134 + 'Did you forget to wrap your parameters in an array?',
135 + DIRECTIVE_SPLICE,
136 + spec[DIRECTIVE_SPLICE]
137 + );
138 + spec[DIRECTIVE_SPLICE].forEach(function(args) {
139 + invariant(
140 + Array.isArray(args),
141 + 'update(): expected spec of %s to be an array of arrays; got %s. ' +
142 + 'Did you forget to wrap your parameters in an array?',
143 + DIRECTIVE_SPLICE,
144 + spec[DIRECTIVE_SPLICE]
145 + );
146 + nextValue.splice.apply(nextValue, args);
147 + });
148 + }
149 +
150 + for (var k in spec) {
151 + if (!ALL_DIRECTIVES_SET[k]) {
152 + nextValue[k] = update(value[k], spec[k]);
153 + }
154 + }
155 +
156 + return nextValue;
157 +}
158 +
159 +module.exports = update;