├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── common.js ├── error.js ├── index.js ├── operations ├── index.js ├── shape.js └── token.js ├── package.json ├── path ├── expand.js ├── intersect.js ├── split.js └── test.js ├── support └── operator.js └── test ├── fixtures └── source.json ├── functional └── index.spec.js └── specs ├── common.spec.js ├── error.spec.js ├── index.spec.js ├── operations ├── index.spec.js └── shape.spec.js ├── path ├── expand.spec.js ├── intersect.spec.js ├── split.spec.js └── test.spec.js └── support └── operator.spec.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es2021": true, 6 | "node": true 7 | }, 8 | "parserOptions": { 9 | "ecmaVersion": 12 10 | }, 11 | "rules": { 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | branches: 5 | only: 6 | - master 7 | notifications: 8 | email: 9 | - jstrimpel@gmail.com -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {2016} {Walmart} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/walmartlabs/json-patchwork.svg?branch=master)](https://travis-ci.org/walmartlabs/json-patchwork) 2 | 3 | # API 4 | This is an implementation of the [JSON Patchwork Specification](#specification-for-implementation). 5 | 6 | ## `patch(target, source, patches)` 7 | Applies patches from a [Patchwork Object](#patchwork-object) to a target document. 8 | 9 | ### Arguments 10 | 1. `target` *(Object or Array)*: The document receiving the patches. 11 | 1. `source` *(Object or Array)*: The document source for the patch values. 12 | 1. `patches` *(Array)*: An array of patches to apply. 13 | - `target` *(Object)* Target configuration for patch. 14 | - `path` *(String)*: The target path for the patch application. 15 | - `tests` *(Array)* An array of tests for equality or values that reduce the target of the patch application. 16 | - `source` *(Object)* Source configuration for patch. 17 | - `path` *(String)*: The source path for the patch application values. 18 | - `tests` *(Array)* An array of tests for equality or values that reduce the source of the patch application. 19 | - [`collect`] *(Boolean)*: Collect the values from the target and the source as opposed to replacing the target values 20 | with source values. 21 | - [`merge`] *(Boolean)*: Merge source value with target value. 22 | - [`unique`] *(Boolean)*: Ensure that the value for a given target is unique, e.g, all array values are unique. 23 | - [`depth`] *(Number)*: The depth to traverse up the source object graph for a source value. The value at the 24 | given depth will be applied to the target document for the patch. Depth must be a negative number. 25 | - [`operations`] *(Array)* Operations to perform on all values for each patch operation a patch definition executes. 26 | 27 | ```javascript 28 | var Patchwork = require('json-patchwork'); 29 | var target = { foo: { bar: 1 } }; 30 | var source = { baz: 2 }; 31 | var patches = [{ 32 | target: { path: '/foo' }, 33 | source: { path: '/' } 34 | merge: true 35 | }]; 36 | 37 | Patchwork.patch(target, source, patches); 38 | /* 39 | Result: 40 | { 41 | foo: { 42 | bar: 1, 43 | baz: 2 44 | } 45 | } 46 | */ 47 | ``` 48 | 49 | ## `register(name, fn)` 50 | Registers an operator that can be configured as part of a patch operation that performs an operation against a patch 51 | value. 52 | 53 | ### Arguments 54 | 1. `name` *(String)*: The name of the operator. 55 | 1. `fn` *(Function)*: The function that will operate on a patch. *MUST* return a value. 56 | - `operation` *(Object)*: Contains patches for a patch operation and meta data. 57 | - `value` *(Object, Array, or Primative)*: The value being modified. 58 | 59 | ```javascript 60 | var Patchwork = require('json-patchwork'); 61 | 62 | Patchwork.register('custom' function (operation, value) { 63 | // do something interesting 64 | return value; 65 | }); 66 | ``` 67 | 68 | ## `unregister(name)` 69 | Registers an operator that can be configured as part of a patch operation that performs an operation against a patch 70 | value. 71 | 72 | ### Arguments 73 | 1. `name` *(String)*: The name of the operator. 74 | 75 | ```javascript 76 | var Patchwork = require('json-patchwork'); 77 | 78 | Patchwork.unregister('custom'); 79 | ``` 80 | 81 | ## `get(input, path, def)` 82 | Get a value using a patchwork compatible path. 83 | 84 | ### Arguments 85 | 1. `input` *(Object)*: Source object. 86 | 1. `path` *(Array|String)*: Key-path to get. 87 | 1. `def` *(Mixed)*: Default value. 88 | 89 | #### Returns 90 | *(Mixed)*: The value for the path. 91 | 92 | ```javascript 93 | var Patchwork = require('json-patchwork'); 94 | var source = { 95 | foo: ['bar', 'baz'] 96 | }; 97 | var val = Patchwork.get(source, '/foo/1') 98 | 99 | /* 100 | Result: 101 | 'baz' 102 | */ 103 | 104 | ``` 105 | 106 | ## `set(input, path, value)` 107 | Set a value using a patchwork compatible path. 108 | 109 | ### Arguments 110 | 1. `input` *(Object)*: Target object. 111 | 1. `path` *(Array|String)*: Key-path to set. 112 | 1. `value` *(Mixed)*: Value to set. 113 | 114 | #### Returns 115 | *(Object)*: The target object. 116 | 117 | ```javascript 118 | var Patchwork = require('json-patchwork'); 119 | var target = { 120 | foo: ['bar', 'baz'] 121 | }; 122 | var val = Patchwork.set(target, '/foo/1', 'something') 123 | 124 | /* 125 | Result: 126 | { 127 | foo: ['bar', 'something'] 128 | } 129 | */ 130 | 131 | ``` 132 | 133 | ## `expand(subject, path, strict, asArray)` 134 | Expand a Patchwork compatible path. 135 | 136 | ### Arguments 137 | 1. `subject` *(Object)*: Source to walk through. 138 | 1. `path` *(String|Array)*: Path to expand. 139 | 1. `strict` *(Boolean)*: Only return valid paths. 140 | 1. `asArray` *(Boolean)*: Return an array instead of a string. 141 | 142 | #### Returns 143 | *(Array)*: The expanded path 144 | 145 | ```javascript 146 | var Patchwork = require('json-patchwork'); 147 | var subject = { 148 | foo: ['bar', 'baz'] 149 | }; 150 | var paths = Patchwork.expand(subject, '/foo/@'); 151 | 152 | /* 153 | Result: 154 | [ '/foo/0', '/foo/1' ] 155 | */ 156 | 157 | ``` 158 | 159 | ## `split(path)` 160 | Split a path; Removes falsy values and ignores escaped path delimiters 161 | 162 | ### Arguments 163 | 1. `path` *(String|Array)*: String path to split. 164 | 165 | #### Returns 166 | *(Array)*: Path parts. 167 | 168 | ```javascript 169 | var Patchwork = require('json-patchwork'); 170 | var path = '/foo/bar/baz'; 171 | var paths = Patchwork.split(path); 172 | 173 | /* 174 | Result: 175 | [ 'foo', 'bar', 'baz' ] 176 | */ 177 | 178 | ``` 179 | 180 | # Included Operators 181 | The following [operators](#operations) are included with JSON Patchwork. 182 | 183 | ## Shape 184 | Used to shape patch values. Virtual patches are used to perform patches on a per shape 185 | basis. 186 | 187 | ```javascript 188 | var Patchwork = require('json-patchwork'); 189 | var source = { 190 | foo: [{ id: 1, prop: 'here' }, { id: 2, prop: 'there' }, { id: 3, prop: 'everywhere' }] 191 | }; 192 | var target = {}; 193 | var patches = [{ 194 | target: { path: '/bar' }, 195 | source: { path: '/foo/@' }, 196 | collect: true, 197 | operations: [{ 198 | type: 'shape', 199 | virtual: { 200 | microverse: [{ 201 | target: { 202 | path: '/microverse' 203 | }, 204 | source: { 205 | path: '/prop', 206 | tests: [{ 207 | path: '/prop', 208 | operator: '==', 209 | value: 'everywhere' 210 | }] 211 | } 212 | }] 213 | }, 214 | shape: { 215 | id: 'id', 216 | b: 'prop', 217 | c: '$microverse' 218 | } 219 | }] 220 | }]; 221 | 222 | Patchwork.patch(target, source, patches); 223 | /* 224 | Result: 225 | { bar: 226 | [ { id: 1, b: 'here', c: null }, 227 | { id: 2, b: 'there', c: null }, 228 | { id: 3, b: 'everywhere', c: 'everywhere' } ] } 229 | */ 230 | ``` 231 | 232 | # Specification for Implementation 233 | The library is based on the JSON Patchwork specification defined below. 234 | 235 | 236 | ## JSON Patchwork v1 237 | JSON Patchwork defines a JSON document structure for expressing a sequence of value patching 238 | operations to apply from a source JSON document to a target JSON document. 239 | 240 | ## Source Documents 241 | Values (patches) are extracted from source documents based upon [patch operations](#patchwork-object). 242 | 243 | ## Target Documents 244 | Values (patches) from the source document are applied to target document based upon 245 | [patch operations](#patchwork-object). 246 | 247 | ## Patchwork Documents 248 | A JSON Patchwork document is a JSON document that represents an array of [patch operation](#patchwork-object) 249 | objects. Each object represents a target to apply a source patch. 250 | 251 | ### Patchwork Object 252 | A JSON Patchwork object maps a (dynamic) target path to instructions to a (dynamic) source path for applying patches. 253 | 254 | #### Target Path 255 | The target path of a patchwork object identifies the path which to apply patches from a source document. 256 | 257 | ```javascript 258 | [{ 259 | "target": { 260 | "path": "/target/path" 261 | } 262 | }] 263 | ``` 264 | 265 | #### Source Path 266 | The source path of a patchwork object identifies the path from which patch values should be resolved in the source document. 267 | 268 | ```javascript 269 | [{ 270 | "target": { 271 | "path": "/target/path" 272 | } 273 | "source": { 274 | "path": "/source/path" 275 | } 276 | }] 277 | ``` 278 | 279 | ## Paths 280 | Paths are used in JSON Patchwork documents to identify a location in a target or source document. 281 | 282 | ### Path Traversal 283 | Forward slashes in JSON Patchwork paths denote traversal into an a document. For example, `/foo/bar` would resolve 284 | to `{ "foo": { "bar": 1 } }` returning the value of the `bar` property. `/foo/bar/baz/0` would resolve to 285 | `{ "foo": { "bar": { "baz": ["some value"] } } }` returning the first element in the `baz`property array. All 286 | [JSON Patch](https://tools.ietf.org/html/rfc6902) paths are valid. 287 | 288 | ### Iterator Token 289 | The `@` character can be used in a path to identify an object or array in a path that should be expanded. In the case of an array, 290 | `/foo/@` would expand `{ "foo": [1, 2] }` to the JSON Patch paths `/foo/0` and `/foo/1`. In the case of an object 291 | `/foo/@` would expand `{ "foo": { "bar": 1, "baz": 2 } }` to the JSON Patch paths `/foo/bar` and `/foo/baz`. 292 | 293 | Iterator tokens can be combined with static path parts to extract single property values as well. For example `/foo/@/baz` would expand 294 | `{ "foo": { "bar": { "baz": 2 }, "foo": { "baz": 2 } } }` to the JSON Patch paths `/foo/bar/baz` and `/foo/foo/baz`. 295 | 296 | Multiple iterator tokens can be used in a path to iterate into a path of objects and arrays. For example `/foo/@/bar/@` would expand 297 | `{ { "foo": { "prop1": { "bar": [1, 2] }, "prop2": { "bar": [1, 2] } } }` to the JSON Patch paths `/foo/prop1/0`, `/foo/prop1/1`, 298 | `/foo/prop2/0`, and `/foo/prop2/1`. 299 | 300 | ### Path Expansion and Patch Application 301 | There are 4 different patching scenarios. If a path doesn't exist during a patch operation it will be created within the limits 302 | of the patching rules. 303 | 304 | #### Static Target Path and Static Source Path 305 | If both the target and source paths are static then the value from the source path is set at the target path. 306 | 307 | ```javascript 308 | var Patchwork = require('json-patchwork'); 309 | var target = {}; 310 | var source = { foo: { bar: 'baz' } }; 311 | var patches = [{ 312 | "target": { 313 | "path": "/here" 314 | }, 315 | "source": { 316 | "path": "/foo" 317 | } 318 | }]; 319 | 320 | Patchwork.patch(target, source, patches); 321 | 322 | /* 323 | Result: 324 | { 325 | { here: { bar: 'baz' } } 326 | } 327 | */ 328 | ``` 329 | 330 | #### Static Target Path and Dynamic Source Path 331 | If the target path is static and the source path is dynamic then source path will be expanded and the value at the last token 332 | will be set at the target path. 333 | 334 | ```javascript 335 | var Patchwork = require('json-patchwork'); 336 | var target = {}; 337 | var source = { foo: [1, 2, 3, 4] }; 338 | var patches = [{ 339 | "target": { 340 | "path": "/here" 341 | }, 342 | "source": { 343 | "path": "/foo/@" 344 | } 345 | }]; 346 | 347 | Patchwork.patch(target, source, patches); 348 | 349 | /* 350 | Result: 351 | { 352 | { here: 4 } 353 | } 354 | */ 355 | ``` 356 | 357 | #### Dynamic Target Path and Static Source Path 358 | If the target path is dynamic and source path is static then the value from the source path is set to the 359 | expanded target paths. 360 | 361 | ```javascript 362 | var Patchwork = require('json-patchwork'); 363 | var target = { here: [1, 2, 3, 4] }; 364 | var source = { foo: [5, 6, 7, 8] }; 365 | var patches = [{ 366 | "target": { 367 | "path": "/here/@" 368 | }, 369 | "source": { 370 | "path": "/foo" 371 | } 372 | }]; 373 | 374 | Patchwork.patch(target, source, patches); 375 | 376 | /* 377 | Result: 378 | { 379 | { here: [ [ 5, 6, 7, 8 ], [ 5, 6, 7, 8 ], [ 5, 6, 7, 8 ], [ 5, 6, 7, 8 ] ] } 380 | } 381 | */ 382 | ``` 383 | 384 | #### Dynamic Target Path and Dynamic Source Path 385 | If both the target and source paths are dynamic then the value from the value at the last token 386 | of the source will be set at each expanded target path. 387 | 388 | ##### Intersection 389 | ```javascript 390 | var Patchwork = require('json-patchwork'); 391 | var target = { foo: [1, 2, 3, 4] }; 392 | var source = { foo: [5, 6, 7, 8, 9] }; 393 | var patches = [{ 394 | "target": { 395 | "path": "/foo/@" 396 | }, 397 | "source": { 398 | "path": "/foo/@" 399 | } 400 | }]; 401 | 402 | Patchwork.patch(target, source, patches); 403 | 404 | /* 405 | Result: 406 | { 407 | { foo: [ 9, 9, 9, 9 ] } 408 | } 409 | */ 410 | ``` 411 | 412 | ### Patch Modifiers 413 | Patch modifiers describe how patch values should be applied in addition to the rules/scenarios defined in 414 | [Paths](#paths). 415 | 416 | #### Merge 417 | If `merge` is `true` the source value is merged into the target value. 418 | 419 | ```javascript 420 | var Patchwork = require('json-patchwork'); 421 | var target = { foo: [1, 2] }; 422 | var source = { bar: [5, 6] }; 423 | var patches = [{ 424 | "merge": true, 425 | "target": { 426 | "path": "/" 427 | }, 428 | "source": { 429 | "path": "/" 430 | } 431 | }]; 432 | 433 | Patchwork.patch(target, source, patches); 434 | 435 | /* 436 | Result: 437 | { 438 | { foo: [ 1, 2 ], bar: [ 5, 6 ] } 439 | } 440 | */ 441 | ``` 442 | 443 | #### Collect 444 | If `collect` is `true` then values are "collected" instead of being replaced. 445 | 446 | ##### Array Example 447 | ```javascript 448 | var Patchwork = require('json-patchwork'); 449 | var target = { foo: [1, 2] }; 450 | var source = { bar: [5, 6] }; 451 | 452 | var patches = [{ 453 | "collect": true, 454 | "target": { 455 | "path": "/foo" 456 | }, 457 | "source": { 458 | "path": "/bar" 459 | } 460 | }]; 461 | 462 | Patchwork.patch(target, source, patches); 463 | 464 | /* 465 | Result: 466 | { 467 | { foo: [ 1, 2, 5, 6 ] } 468 | } 469 | */ 470 | ``` 471 | 472 | ##### Object Example 473 | Collected objects are transformed into array like objects. 474 | 475 | ```javascript 476 | var Patchwork = require('json-patchwork'); 477 | var target = {}; 478 | var source = { foo: 1, bar: 2, baz: 3 }; 479 | 480 | var patches = [{ 481 | "collect": true, 482 | "target": { 483 | "path": "/" 484 | }, 485 | "source": { 486 | "path": "/@" 487 | } 488 | }]; 489 | 490 | Patchwork.patch(target, source, patches); 491 | 492 | /* 493 | Result: 494 | { 495 | { '0': 1, '1': 2, '2': 3, length: 3 } 496 | } 497 | */ 498 | ``` 499 | 500 | #### Depth 501 | The depth modifier specifies a distance to traverse up the source object graph path from the source value. This path is applied to 502 | the target path when the patch operation is applied. Depth must be a negative number. 503 | 504 | ```javascript 505 | var Patchwork = require('json-patchwork'); 506 | var target = { foo: { bar: [], baz: [] } }; 507 | var source = { foo: { bar: [ [ { here: 1 } ] ], baz: [ [ { there: 2 } ] ] } }; 508 | var patches = [{ 509 | "depth": -2, 510 | "target": { 511 | "path": "/foo/@" 512 | }, 513 | "source": { 514 | "path": "/foo/@/@/@" 515 | } 516 | }]; 517 | 518 | Patchwork.patch(target, source, patches); 519 | 520 | /* 521 | Result: 522 | { 523 | "foo": { 524 | "bar": [ 525 | [ 526 | { 527 | "there": 2 528 | } 529 | ] 530 | ], 531 | "baz": [ 532 | [ 533 | { 534 | "there": 2 535 | } 536 | ] 537 | ] 538 | } 539 | } 540 | */ 541 | ``` 542 | 543 | ### Operations 544 | The `operations` property of the patchwork object contains instructions for altering patch values. 545 | 546 | ```javascript 547 | [{ 548 | "target": { "path": "/target/path" }, 549 | "source": { "path": "/source/path" } 550 | "operations": [{}] 551 | }] 552 | ``` 553 | 554 | #### Example Operation 555 | ```javascript 556 | [{ 557 | "target": { "path": "/target/path" }, 558 | "source": { "path": "/source/path" } 559 | operations: [{ 560 | type: 'shape', 561 | shape: { 562 | id: 'id', 563 | a: 'prop' 564 | } 565 | }] 566 | }] 567 | ``` 568 | 569 | The specification implementation should pass the operation definition and the value to a function that implements 570 | the operation. The implementation should return the modified value. 571 | 572 | ### Tests 573 | An array of tests for equality or values that reduce the target or source of a patch application. 574 | 575 | ```javascript 576 | [{ 577 | "target": { "path": "/target/path", "tests": [] }, 578 | "source": { "path": "/source/path", "tests": [] } 579 | }] 580 | ``` 581 | 582 | #### Example Test 583 | ```javascript 584 | [{ 585 | "target": { "path": "/target/path" }, 586 | "source": { 587 | "path": "/source/path", 588 | "tests": [{ 589 | path: '/prop', 590 | operator: '==', 591 | value: 'everywhere' 592 | }] 593 | } 594 | }] 595 | ``` 596 | 597 | #### Supported Test Operators 598 | This is not a final draft. The list is limited to the required use cases upon specification draft. Tests will likely expand and change. 599 | 600 | * '==': equals 601 | * '!=': not equals 602 | * '===': strict equals 603 | * '!==': strict not equals 604 | * '~': regular expression 605 | * '!~': negated regular expression 606 | * 'in': in an array or object 607 | * 'notIn' not in an array or object 608 | 609 | Wrapping the above test operators in parens () will compare the same path in the source and target. 610 | Example: 611 | ```javascript 612 | { 613 | "target": { 614 | "path": "/path/@" 615 | }, 616 | "source": { 617 | "path": "/path/@", 618 | "tests": [ 619 | { 620 | "path": "/path/@/target", 621 | "operator": "(==)", 622 | "value": "/path/@/path/target/" 623 | } 624 | ] 625 | } 626 | } 627 | ``` 628 | -------------------------------------------------------------------------------- /common.js: -------------------------------------------------------------------------------- 1 | /** @see http://lodash.com/docs#has */ 2 | exports.has = require('lodash/has'); 3 | 4 | /** @see http://lodash.com/docs#get */ 5 | exports.get = require('lodash/get'); 6 | 7 | /** @see http://lodash.com/docs#set */ 8 | exports.set = require('lodash/set'); 9 | 10 | /** @see http://lodash.com/docs#update */ 11 | exports.update = require('lodash/update'); 12 | 13 | /** @see http://lodash.com/docs#extend */ 14 | exports.extend = require('lodash/extend'); 15 | 16 | /** @see http://lodash.com/docs#uniqWith */ 17 | exports.uniqWith = require('lodash/uniqWith'); 18 | 19 | /** @see http://lodash.com/docs#isEqual */ 20 | exports.isEqual = require('lodash/isEqual'); 21 | 22 | /** @see http://lodash.com/docs#some */ 23 | exports.some = require('lodash/some'); 24 | 25 | /** @see http://lodash.com/docs#every */ 26 | exports.every = require('lodash/every'); 27 | 28 | /** @see http://lodash.com/docs#map */ 29 | exports.map = require('lodash/map'); 30 | 31 | /** @see http://lodash.com/docs#mapValues */ 32 | exports.mapValues = require('lodash/mapValues'); 33 | 34 | /** @see http://lodash.com/docs#flatMap */ 35 | exports.flatMap = require('lodash/flatMap'); 36 | 37 | /** @see http://lodash.com/docs#partial */ 38 | exports.partial = require('lodash/partial'); 39 | 40 | /** @see http://lodash.com/docs#find */ 41 | exports.find = require('lodash/find'); 42 | 43 | /** @see http://lodash.com/docs#filter */ 44 | exports.filter = require('lodash/filter'); 45 | 46 | /** 47 | * Regular expression used for matching leading and 48 | * trailing whitespace... 49 | * @type {RegEx} 50 | */ 51 | var RX_TRIM_WHITESPACE = /^\s+|\s+$/g; 52 | 53 | /** 54 | * Remove leading and trailing whitespace. 55 | * @param {String} input Value to trim. 56 | * @returns {String} 57 | */ 58 | exports.trim = function trim(input) { 59 | return String(input).replace(RX_TRIM_WHITESPACE, ''); 60 | }; 61 | 62 | /** 63 | * Returns whether supplied value is a `RegExp` 64 | * @param {mixed} input Value to check. 65 | * @returns {Boolean} 66 | */ 67 | exports.isRegExp = function isRegExp(input) { 68 | return '[object RegExp]' === asString(input); 69 | }; 70 | 71 | /** 72 | * Returns whether supplied value is an `Array` 73 | * @param {mixed} input Value to check. 74 | * @returns {Boolean} 75 | */ 76 | exports.isArray = function isArray(input) { 77 | return '[object Array]' === asString(input); 78 | }; 79 | 80 | /** 81 | * Returns whether supplied value is an `Array` 82 | * @param {mixed} input Value to check. 83 | * @returns {Boolean} 84 | */ 85 | exports.isArrayLike = function isArrayLike(input) { 86 | return ( 87 | exports.isArray(input) || 88 | (exports.isObject(input) && input.length > 0 && input.length < Number.MAX_SAFE_INTEGER) 89 | ); 90 | }; 91 | 92 | /** 93 | * Returns whether supplied value is a POJO `Object` 94 | * @param {mixed} input Value to check. 95 | * @returns {Boolean} 96 | */ 97 | exports.isPlainObject = function isPlainObject(input) { 98 | return '[object Object]' === asString(input); 99 | }; 100 | 101 | /** 102 | * Returns whether supplied value is an `Object` 103 | * @param {mixed} input Value to check. 104 | * @returns {Boolean} 105 | */ 106 | exports.isObject = function isObject(input) { 107 | return input instanceof Object; 108 | }; 109 | 110 | /** 111 | * Convert a value to an integer. 112 | * @param {mixed} input Value to convert. 113 | * @param {mixed} def Default value. 114 | * @param {Number} radix Radix to use in conversion. Default: 10 115 | * @returns {Number} 116 | */ 117 | exports.toInt = function toInt(input, def, radix) { 118 | return parseInt(input, radix || 10) || def; 119 | }; 120 | 121 | /** 122 | * Convert an array-like value to an array. 123 | * @param {mixed} input Value to convert. 124 | * @returns {Number} 125 | */ 126 | exports.toArray = function toInt(input) { 127 | return Array.prototype.slice.call(input); 128 | }; 129 | 130 | /** 131 | * Convert an object to it's string interpretation. 132 | * @param {Object} value Object to convert to string. 133 | * @returns {String} 134 | */ 135 | function asString(value) { 136 | return Object.prototype.toString.call(value); 137 | } 138 | -------------------------------------------------------------------------------- /error.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A generic Patchwork error. 3 | * @constructor 4 | * @param {(Error|String)} message Message for error. 5 | */ 6 | module.exports = function PatchworkError(message) { 7 | if (this instanceof PatchworkError === false) { 8 | return new PatchworkError(message); 9 | } 10 | 11 | this.constructor.prototype.__proto__ = Error.prototype; 12 | 13 | Error.call(this); 14 | Error.captureStackTrace(this, this.constructor); 15 | 16 | this.name = this.constructor.name; 17 | this.message = message instanceof Error ? 18 | message.message : message; 19 | }; 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var _ = require('./common'); 2 | var split = require('./path/split'); 3 | var expand = require('./path/expand'); 4 | var testPath = require('./path/test'); 5 | var Operations = require('./operations'); 6 | 7 | /** @type {Function} */ 8 | exports.expand = expand; 9 | 10 | /** @type {Function} */ 11 | exports.patch = patch; 12 | 13 | /** @type {Function} */ 14 | exports.register = Operations.register; 15 | 16 | /** @type {Function} */ 17 | exports.unregister = Operations.unregister; 18 | 19 | /** @type {Function} */ 20 | exports.get = get; 21 | 22 | /** @type {Function} */ 23 | exports.set = set; 24 | 25 | /** @type {Function} */ 26 | exports.split = split; 27 | 28 | /** @type {Function} */ 29 | exports.testPath = testPath; 30 | 31 | /** 32 | * Patch a supplied target using the supplied source. 33 | * @param {Object} target Target object. 34 | * @param {Object} source Source object. 35 | * @param {Array} patches Patches to run. 36 | * @param {Array} log Array to push entries into. 37 | * @returns {Object} 38 | */ 39 | function patch(target, source, patches, log, ctx) { 40 | var dirty = false; 41 | 42 | if (!_.isArray(patches)) { 43 | return dirty; 44 | } 45 | 46 | for (var patchIdx = 0; patchIdx < patches.length; patchIdx++) { 47 | // Globals 48 | var patch = Object.assign({}, { 49 | source: {}, 50 | target: {}, 51 | operations: [] 52 | }, patches[patchIdx]); 53 | 54 | var merge = patch.merge; 55 | var depth = _.toInt(patch.depth, 0); 56 | var collect = patch.collect; 57 | var unique = patch.unique; 58 | var operations = patch.operations; 59 | var targetAsSource = patch.targetAsSource; 60 | 61 | // Source 62 | var unresolvedSourcePath = patch.source.path; 63 | var sourceIsRoot = unresolvedSourcePath === '/'; 64 | var sourcePaths = sourceIsRoot ? [undefined] : // this is essentially a noop to symbolize a root 65 | expand(source, unresolvedSourcePath, true, true); // paths must exist 66 | var sourceTest = patch.source.tests; 67 | 68 | // Target 69 | var unresolvedTargetPath = patch.target.path; 70 | var targetIsRoot = unresolvedTargetPath === '/'; 71 | var targetPaths = targetIsRoot ? [undefined] : // this is essentially a noop to symbolize a root 72 | expand(target, unresolvedTargetPath, false, true); // path existance is optional 73 | var targetTest = patch.target.tests; 74 | 75 | // Updater 76 | var transformValue = function(sourcePath, sourceValue, targetPath, targetValue, isRoot) { 77 | isRoot = !!isRoot; 78 | 79 | // Integration of the regex validity for value/object conformity; 80 | // Returns `targetValue` should this not pass... 81 | if (targetTest && testPath(targetTest, target, source, targetPath, sourcePath, unresolvedTargetPath, unresolvedSourcePath) === false) { 82 | return targetValue; 83 | } 84 | 85 | var newValue = targetAsSource ? targetValue : sourceValue; 86 | 87 | if (_.isArray(operations)) { 88 | for (var operationIdx = 0; operationIdx < operations.length; operationIdx++) { 89 | newValue = Operations.execute(operations[operationIdx], newValue, ctx); 90 | } 91 | } 92 | 93 | var cleanValue = newValue; 94 | 95 | // Explicitly merge `newValue` over `targetValue` replacing 96 | // any object keys or collection indices with the 97 | // appropriate values; 98 | if (merge && _.isObject(newValue) && _.isObject(targetValue)) { 99 | newValue = _.extend( 100 | _.isArray(targetValue) ? [] : {}, 101 | targetValue, 102 | newValue 103 | ); 104 | } 105 | 106 | // Create or add to an existing collection 107 | if (collect === true) { 108 | if (targetValue !== undefined) { 109 | if (isRoot && _.isPlainObject(targetValue)) { 110 | if (_.isArrayLike(targetValue)) { 111 | var newValueArray = _.toArray(targetValue).concat(newValue); 112 | var newArrayLikeValue = { 113 | length: newValueArray.length 114 | }; 115 | 116 | for (var nvArrayIdx = 0; nvArrayIdx < newValueArray.length; nvArrayIdx++) { 117 | newArrayLikeValue[nvArrayIdx] = newValueArray[nvArrayIdx]; 118 | } 119 | 120 | newValue = newArrayLikeValue; 121 | } else { 122 | newValue = { 123 | 0: newValue, 124 | length: 1 125 | }; 126 | } 127 | } else { 128 | newValue = (_.isArray(targetValue) ? targetValue : [targetValue]).concat(newValue); 129 | } 130 | } else { 131 | newValue = _.isArray(newValue) ? newValue : [newValue]; 132 | } 133 | } 134 | 135 | // Remove non-unique values from a given collection 136 | if (unique && _.isArray(newValue) && newValue.length > 1) { 137 | newValue = _.uniqWith(newValue, _.isEqual); 138 | } 139 | 140 | dirty = true; 141 | 142 | if (_.isArray(log)) { 143 | log.push({ 144 | to: targetPath, 145 | from: sourcePath, 146 | value: cleanValue, 147 | patch: patch 148 | }); 149 | } 150 | 151 | return newValue; 152 | }; 153 | 154 | for (var sourcePathIdx = 0, sourceLen = targetAsSource ? 1 : sourcePaths.length; sourcePathIdx < sourceLen; sourcePathIdx++) { 155 | var sourcePath = sourcePaths[sourcePathIdx]; 156 | var sourceValue = sourcePath === undefined ? source : _.get(source, sourcePath); 157 | 158 | for (var targetPathIdx = 0; targetPathIdx < targetPaths.length; targetPathIdx++) { 159 | var targetPath = targetIsRoot ? [] : targetPaths[targetPathIdx]; 160 | 161 | if (depth < 0) { 162 | targetPath = targetPath.concat(sourcePath.slice(depth)); 163 | } 164 | 165 | // Integration of the regex validity for value/object conformity; 166 | // Skip single source patch operation should this not pass... 167 | if (sourceTest && testPath(sourceTest, source, target, sourcePath, targetPath, unresolvedSourcePath, unresolvedTargetPath) === false) { 168 | continue; 169 | } 170 | 171 | if (targetPath.length === 0) { 172 | var newRootValue = transformValue(sourcePath, sourceValue, targetPath, target, true); 173 | 174 | if (_.isArray(target)) { 175 | target.splice.apply(target, [0, target.length].concat(newRootValue)); 176 | } else if (_.isPlainObject(target)) { 177 | for (var keyIdx = 0, keys = Object.keys(target); keyIdx < keys.length; keyIdx++) { 178 | delete target[keys[keyIdx]]; 179 | } 180 | 181 | _.extend(target, newRootValue); 182 | } 183 | } else { 184 | _.update(target, targetPath, _.partial(transformValue, sourcePath, sourceValue, targetPath)); 185 | } 186 | } 187 | } 188 | } 189 | 190 | return dirty; 191 | } 192 | 193 | /** 194 | * Get a value using a patchwork compatible path. 195 | * @param {Object} input Source object. 196 | * @param {(Array|String)} path Key-path to get. 197 | * @param {mixed} def Default value. 198 | * @returns {mixed} 199 | */ 200 | function get(input, path, def) { 201 | path = split(path); 202 | 203 | return path.length ? _.get(input, path, def) : input; 204 | } 205 | 206 | /** 207 | * Set a value using a patchwork compatible path. 208 | * @param {Object} input Target object. 209 | * @param {(Array|String)} path Key-path to set. 210 | * @param {mixed} value Value to set. 211 | * @returns {Object} 212 | */ 213 | function set(input, path, value) { 214 | path = split(path); 215 | 216 | return path.length ? _.set(input, path, value) : value; 217 | } 218 | -------------------------------------------------------------------------------- /operations/index.js: -------------------------------------------------------------------------------- 1 | var _ = require('../common'); 2 | var PatchworkError = require('../error'); 3 | var shape = require('./shape'); 4 | 5 | /** @type {Function} */ 6 | exports.execute = execute; 7 | 8 | /** @type {Function} */ 9 | exports.register = register; 10 | 11 | /** @type {Function} */ 12 | exports.registered = registered; 13 | 14 | /** @type {Function} */ 15 | exports.unregister = unregister; 16 | 17 | /** 18 | * Global operation store. 19 | * @type {Object} 20 | */ 21 | var registry = { 22 | core: { 23 | shape: shape 24 | }, 25 | local: {} 26 | }; 27 | 28 | /** 29 | * Execute a registered operation. 30 | * @param {Object} operation An operation object. 31 | * @param {Object} input An object to operator on. 32 | * @returns {Object} 33 | */ 34 | function execute(operation, input, ctx) { 35 | var type = formatType(operation.type); 36 | var fn; 37 | 38 | if (_.has(registry.local, type)) { 39 | fn = registry.local[type]; 40 | } else if (_.has(registry.core, type)) { 41 | fn = registry.core[type]; 42 | } else { 43 | throw new PatchworkError('Operator [' + type + '] is not registered.'); 44 | } 45 | 46 | return fn(operation, input, ctx); 47 | } 48 | 49 | /** 50 | * Register a local operation. 51 | * @override 52 | * @param {string} type Operation name. 53 | * @param {Function} fn Operation implementation. 54 | */ 55 | function register(type, fn) { 56 | type = formatType(type); 57 | registry.local[type] = fn; 58 | } 59 | 60 | /** 61 | * Checks to see if an operation is registered. 62 | * @param {String} type Name of operation to check. 63 | * @returns {Boolean} 64 | */ 65 | function registered(type) { 66 | var isRegistered = _.has(registry.local, type); 67 | 68 | if (isRegistered === false) { 69 | isRegistered = _.has(registry.core, type); 70 | } 71 | 72 | return isRegistered; 73 | } 74 | 75 | /** 76 | * Unregister a local operation. 77 | * @override 78 | * @param {string} type Operation name. 79 | */ 80 | function unregister(type) { 81 | type = formatType(type); 82 | 83 | if (_.has(registry.local, type) === false) { 84 | throw new PatchworkError('Operator [' + type + '] is not registered.'); 85 | } 86 | 87 | delete registry.local[type]; 88 | } 89 | 90 | /** 91 | * Format a supplied type. 92 | * @param {string} type Operation name. 93 | * @returns {string} 94 | */ 95 | function formatType(type) { 96 | return type.toLowerCase(); 97 | } 98 | -------------------------------------------------------------------------------- /operations/shape.js: -------------------------------------------------------------------------------- 1 | var _ = require('../common'); 2 | var expand = require('../path/expand'); 3 | var Patchwork = require('../index'); 4 | var token = require('./token'); 5 | 6 | module.exports = shape; 7 | 8 | /** 9 | * Shape an object based on a set criterion. 10 | * @param {Object} operation Operation that triggered execution. 11 | * @param {Object} source Value to execute on. 12 | * @param {Object} virtual Virtual fields. 13 | * @returns {Object} 14 | */ 15 | function shape(operation, source, ctx, virtual) { 16 | virtual = virtual || operation.virtual && patchVirtual(source, operation.virtual); 17 | 18 | var shapeObj = operation.shape; 19 | 20 | if (_.isPlainObject(shapeObj)) { 21 | return _.mapValues(shapeObj, function(value) { 22 | return shape(_.extend({}, shapeObj, { 23 | shape: value 24 | }), source, ctx, virtual); 25 | }); 26 | } 27 | 28 | // In a given shapeObj the "@" value refers to the current source passed to 29 | // the shape... 30 | else if (shapeObj === '@') { 31 | return source; 32 | } 33 | 34 | var path, def = null; 35 | 36 | if (_.isArray(shapeObj)) { 37 | path = shapeObj[0]; 38 | def = token.replace(shapeObj[1], source); 39 | } else { 40 | path = shapeObj; 41 | } 42 | 43 | path = _.trim(path); 44 | 45 | if (virtual && path.charAt(0) === '$') { 46 | var virtualValue = virtual[path.substr(1)]; 47 | 48 | return virtualValue === undefined ? def : virtualValue; 49 | } 50 | 51 | var paths = expand(source, path, true, true); 52 | 53 | // If we have multiple paths for a given key-path we just grab 54 | // a map of the values to make things simple, we should 55 | // probably allow for more complex scenarios but this is fine 56 | // for now... 57 | if (paths.length > 1) { 58 | return _.map(paths, function(path) { 59 | return _.get(source, path, def); 60 | }); 61 | } 62 | 63 | return _.get(source, paths[0], def); 64 | } 65 | 66 | /** 67 | * Runs Patchwork over generated patches, 68 | * @param {Object} source Source object. 69 | * @param {Object} virtual Virtual fields. 70 | * @returns {Object} 71 | */ 72 | function patchVirtual(source, virtual) { 73 | return _.mapValues(virtual, function(patches, id) { 74 | var value = {}; 75 | 76 | Patchwork.patch(value, source, patches); 77 | 78 | return value[id]; 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /operations/token.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var split = require('../path/split'); 3 | 4 | /** @type {Function} */ 5 | exports.replace = replace; 6 | 7 | /** @type {RegExp} */ 8 | var TOKEN_EXP = /<%([^%>]*)%>/g; 9 | 10 | /** 11 | * Replace tokens 12 | * @param {String} input String with tokens. 13 | * @param {Object} source Source object. 14 | * @returns {mixed} 15 | */ 16 | function replace(input, source) { 17 | var output = input; 18 | 19 | if (_.isString(output)) { 20 | var tokenMatch = ~output.indexOf('<%') && 21 | ~output.indexOf('%>') && 22 | TOKEN_EXP.test(output); 23 | 24 | if (tokenMatch) { 25 | output = output.replace(TOKEN_EXP, function(match, token) { 26 | return _.get(source, split(_.trim(token)), ''); 27 | }); 28 | } 29 | } 30 | 31 | return output; 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-patchwork", 3 | "contributors": [ 4 | "Seth Benjamin ", 5 | "Maxime Najim ", 6 | "Jason Strimpel " 7 | ], 8 | "version": "1.2.1", 9 | "description": "Implementation of JSON Patchwork, which defines a JSON document structure for expressing a sequence of value patching operations to apply from a source JSON document to a target JSON document.", 10 | "keywords": [ 11 | "JSON patch", 12 | "JSON", 13 | "patch", 14 | "configuration" 15 | ], 16 | "license": "Apache-2.0", 17 | "main": "index.js", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/walmartlabs/json-patchwork" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/walmartlabs/json-patchwork/issues" 24 | }, 25 | "scripts": { 26 | "lint": "eslint .", 27 | "test": "istanbul test _mocha -- -R spec --recursive ./test", 28 | "test-specs": "istanbul test _mocha -- -R spec --recursive ./test/specs", 29 | "test-functional": "istanbul test _mocha -- -R spec --recursive ./test/functional", 30 | "cover": "npm test --coverage" 31 | }, 32 | "dependencies": { 33 | "lodash": "^4.12.0" 34 | }, 35 | "devDependencies": { 36 | "chai": "^4.2.0", 37 | "eslint": "^7.8.1", 38 | "istanbul": "^0.4.3", 39 | "mocha": "^8.1.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /path/expand.js: -------------------------------------------------------------------------------- 1 | var _ = require('../common'); 2 | var split = require('../path/split'); 3 | 4 | module.exports = expand; 5 | 6 | /** 7 | * Expand a Patchwork compatible path. 8 | * @param {Object} subject Source to walk through. 9 | * @param {(String|Array)} path Path to expand. 10 | * @param {Boolean} strict Only return valid paths. 11 | * @param {Boolean} asArray Return an array instead of a string. 12 | * @returns {(String|Array)} 13 | */ 14 | function expand(subject, path, strict, asArray) { 15 | if (path === undefined) { 16 | return []; 17 | } 18 | 19 | var expanded = []; 20 | var parts = split(path); 21 | var expandedIdx, keyIdx, expIdx, keys; 22 | 23 | for (var partIdx = 0; partIdx < parts.length; partIdx++) { 24 | var part = parts[partIdx]; 25 | 26 | // Handles non-dynamic path parts, we are just mapping the current part 27 | // over the previous parts that exist in `expanded`... 28 | if (part !== '@') { 29 | if (expanded.length) { 30 | for (expandedIdx = 0; expandedIdx < expanded.length; expandedIdx++) { 31 | expanded[expandedIdx] = expanded[expandedIdx].concat(part); 32 | } 33 | } else { 34 | expanded = [ 35 | [part] 36 | ]; 37 | } 38 | } 39 | 40 | // Handle cases were we are expanding from a root such as `/@` by either collecting 41 | // keys or array indices... 42 | else if (expanded.length === 0) { 43 | if (_.isArray(subject)) { 44 | for (var subjectIdx = 0; subjectIdx < subject.length; subjectIdx++) { 45 | expanded[subjectIdx] = [subjectIdx]; 46 | } 47 | } else { 48 | keys = Object.keys(subject); 49 | 50 | for (keyIdx = 0; keyIdx < keys.length; keyIdx++) { 51 | expanded[keyIdx] = [keys[keyIdx]]; 52 | } 53 | } 54 | } 55 | 56 | // Handle dynamic `@` parts follws the same precendet as the non-dynamic 57 | // parts with the difference being we grab sources at a given path to get 58 | // the next parts required... 59 | else { 60 | var newExpanded = []; 61 | 62 | for (expandedIdx = 0; expandedIdx < expanded.length; expandedIdx++) { 63 | var current = expanded[expandedIdx]; 64 | var source = _.get(subject, current); 65 | 66 | if (_.isArray(source)) { 67 | for (var sourceIdx = 0; sourceIdx < source.length; sourceIdx++) { 68 | newExpanded.push(current.concat(sourceIdx)); 69 | } 70 | } else if (_.isPlainObject(source)) { 71 | keys = Object.keys(source); 72 | 73 | for (keyIdx = 0; keyIdx < keys.length; keyIdx++) { 74 | newExpanded.push(current.concat(keys[keyIdx])); 75 | } 76 | } 77 | } 78 | 79 | expanded = newExpanded; 80 | } 81 | } 82 | 83 | // When `true` will filter for paths that exist in a given expansion, 84 | // this should probably be optimized and/or moved into the path expansion 85 | // so it all happens in on operation... 86 | if (strict) { 87 | for (expIdx = 0; expIdx < expanded.length; expIdx++) { 88 | if (_.has(subject, expanded[expIdx]) === false) { 89 | expanded.splice(expIdx, 1); 90 | } 91 | } 92 | } 93 | 94 | if (asArray) { 95 | return expanded; 96 | } 97 | 98 | for (expIdx = 0; expIdx < expanded.length; expIdx++) { 99 | expanded[expIdx] = '/' + expanded[expIdx].join('/'); 100 | } 101 | 102 | return expanded.length ? expanded : [path]; 103 | } 104 | -------------------------------------------------------------------------------- /path/intersect.js: -------------------------------------------------------------------------------- 1 | var split = require('../path/split'); 2 | 3 | module.exports = intersect; 4 | 5 | /** 6 | * Intersect two Patchwork compatible paths. 7 | * @param {(String|Array)} resolvedPath Resolved path for collectin overlapping members. 8 | * @param {(String|Array)} leftPath Unresolved left-hand path. 9 | * @param {(String|Array)} rightPath Unresolved right-hand path. 10 | * @returns {Array} 11 | */ 12 | function intersect(resolvedPath, leftPath, rightPath) { 13 | resolvedPath = split(resolvedPath); 14 | 15 | if (!~leftPath.indexOf('@') && !~rightPath.indexOf('@')) { 16 | return resolvedPath; 17 | } 18 | 19 | var leftPathCmp = split(leftPath); 20 | var rightPathCmp = split(rightPath); 21 | var intersection = []; 22 | 23 | for (var srcCmpIdx = 0; srcCmpIdx < leftPathCmp.length; srcCmpIdx++) { 24 | // The moment we encounter a non-equal path member we should not continue, 25 | // we can only reconcile against overlapping members... 26 | if (leftPathCmp[srcCmpIdx] !== '@' && rightPathCmp[srcCmpIdx] !== leftPathCmp[srcCmpIdx]) { 27 | break; 28 | } 29 | 30 | intersection.push(resolvedPath[srcCmpIdx]); 31 | } 32 | 33 | // Apply the overlap from the left-hand-side of the leftPath components, 34 | // this can still produce @'ed dynamic paths that need to be handled 35 | // accordingly... 36 | if (intersection.length) { 37 | leftPathCmp.splice.apply(leftPathCmp, [0, intersection.length].concat(intersection)); 38 | } 39 | 40 | return leftPathCmp; 41 | } 42 | -------------------------------------------------------------------------------- /path/split.js: -------------------------------------------------------------------------------- 1 | var _ = require('../common'); 2 | 3 | module.exports = split; 4 | 5 | /** @type {RegEx} */ 6 | var RX_PATH_DELIMITER = /(\\.|[^\/])+/g; 7 | 8 | /** 9 | * Split a path; Removes falsy values and ignores 10 | * escaped path delimiters 11 | * @param {(String|Array)} path String path to split. 12 | * @returns {Array} 13 | */ 14 | function split(path) { 15 | if (_.isArray(path)) { 16 | return path; 17 | } 18 | 19 | var filtered = []; 20 | var parts = path.match(RX_PATH_DELIMITER); 21 | 22 | if (parts && parts.length) { 23 | for (var partIdx = 0; partIdx < parts.length; partIdx++) { 24 | filtered.push(parts[partIdx].replace(/\\/, '')); 25 | } 26 | } 27 | 28 | return filtered; 29 | } 30 | -------------------------------------------------------------------------------- /path/test.js: -------------------------------------------------------------------------------- 1 | var _ = require('../common'); 2 | var expand = require('./expand'); 3 | var intersect = require('./intersect'); 4 | var operator = require('../support/operator'); 5 | 6 | module.exports = test; 7 | 8 | /** 9 | * Test a path for conformity. 10 | * @param {(Array)} tests Array containing data for checking conformity. 11 | * @param {Object} source Source object to validate. 12 | * @param {Object} target Target object to validate. 13 | * @param {(String|Array)} sourcePath Source path. 14 | * @param {(String|Array)} targetPath Target path. 15 | * @param {(String|Array)} unresolvedSourcePath Unresolved source path. 16 | * @param {(String|Array)} unresolvedTargetPath Unresolved target path. 17 | * @returns {Boolean} 18 | */ 19 | function test(tests, source, target, sourcePath, targetPath, unresolvedSourcePath, unresolvedTargetPath) { 20 | tests = _.isArray(tests) ? tests : [tests]; 21 | 22 | return _.every(tests, function(test) { 23 | test = _.isArray(test) ? test : [test]; 24 | 25 | return _.some(test, function(testConfig) { 26 | var compare = operator(testConfig.operator); 27 | var ruleSourcePath = intersect(sourcePath, testConfig.path, unresolvedSourcePath); 28 | var didExpandSourcePath = false; 29 | var ruleSourceValue; 30 | 31 | if (~ruleSourcePath.indexOf('@')) { 32 | var ruleSourcePathExpanded = expand(source, ruleSourcePath, true, true); 33 | 34 | didExpandSourcePath = true; 35 | ruleSourceValue = _.flatMap(ruleSourcePathExpanded, _.partial(_.get, source)); 36 | } else { 37 | ruleSourceValue = _.get(source, ruleSourcePath); 38 | } 39 | 40 | var ruleTargetValue = testConfig.value; 41 | 42 | if (compare.isPath()) { 43 | var ruleTargetPath = intersect(targetPath, ruleTargetValue, unresolvedTargetPath); 44 | var ruleTargetPathExpanded = expand(target, ruleTargetPath, true, true); 45 | 46 | return _.some(ruleTargetPathExpanded, function(path) { 47 | return compare(ruleSourceValue, _.get(target, path)); 48 | }); 49 | } 50 | 51 | // For the time being we are just checking if any of the mapped 52 | // source values match the target value, it's basically an OR 53 | // operation, this should probably be configurable but this is okay 54 | // for now... 55 | if (didExpandSourcePath) { 56 | return _.some(ruleSourceValue, _.partial(compare, ruleTargetValue)); 57 | } 58 | 59 | return compare(ruleSourceValue, ruleTargetValue); 60 | }); 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /support/operator.js: -------------------------------------------------------------------------------- 1 | var _ = require('../common'); 2 | var PatchworkError = require('../error'); 3 | var operators = ['==', '!=', '===', '!==', '~', '!~', 'in', 'notIn']; 4 | 5 | module.exports = operator; 6 | 7 | /** 8 | * Creates a function that compares two values. 9 | * @param {Object} operator An object containing the operator configuration. 10 | * @returns {Function} 11 | */ 12 | function operator(operator) { 13 | var compiled = compile(operator); 14 | var comparitor = function(type, left, right) { 15 | /* istanbul ignore else */ 16 | if (type == '==') { 17 | return left == right; 18 | } else if (type == '===') { 19 | return left === right; 20 | } else if (type == '~') { 21 | var regex = _.isRegExp(right) ? right : new RegExp(right); 22 | 23 | return regex.test(String(left)) === false ? false : true; 24 | } else if (type == 'in') { 25 | return _.find(right, function(current) { 26 | return current == left; 27 | }) !== undefined; 28 | } else if (type == '!=') { 29 | return !comparitor('==', left, right); 30 | } else if (type == '!==') { 31 | return !comparitor('===', left, right); 32 | } else if (type == 'notIn') { 33 | return !comparitor('in', left, right); 34 | } else if (type == '!~') { 35 | return !comparitor('~', left, right); 36 | } 37 | }; 38 | 39 | var compare = _.partial(comparitor, compiled.type); 40 | 41 | /** 42 | * Get the comparitor type. 43 | * @returns {String} 44 | */ 45 | compare.getType = function() { 46 | return compiled.type; 47 | }; 48 | 49 | /** 50 | * get the comparitor path flag. 51 | * @returns {Boolean} 52 | */ 53 | compare.isPath = function() { 54 | return compiled.isPath; 55 | }; 56 | 57 | return compare; 58 | } 59 | 60 | /** 61 | * Compiles a supplied operator type. 62 | * @param {String} operator Operator to compile. 63 | * @returns {Object} Compiled operator flags. 64 | */ 65 | function compile(operator) { 66 | var formatted = _.trim(operator); 67 | var isPath = ( 68 | (formatted.indexOf('(') === 0) && 69 | (formatted.lastIndexOf(')') === (formatted.length - 1)) 70 | ); 71 | 72 | if (isPath) { 73 | formatted = formatted.substr(1, formatted.length - 2); 74 | } 75 | 76 | if (!~operators.indexOf(formatted)) { 77 | throw new PatchworkError('Invalid operator [' + formatted + '] supplied.'); 78 | } 79 | 80 | return { 81 | type: formatted, 82 | isPath: isPath 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /test/fixtures/source.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": { 3 | "bar": 1, 4 | "baz": 2, 5 | "qix": [ 6 | 3, 4 7 | ] 8 | }, 9 | "hello": { 10 | "world": "lorem" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/functional/index.spec.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var expect = chai.expect; 3 | var _ = require('lodash'); 4 | var Patchwork = require('../../index'); 5 | 6 | describe('patch', function() { 7 | it('should patch a static source path to a static target path', function() { 8 | var patches = [{ 9 | target: { 10 | path: '/baz' 11 | }, 12 | source: { 13 | path: '/foo/bar' 14 | } 15 | }]; 16 | var target = {}; 17 | var source = { 18 | foo: { 19 | bar: [1, 2, 3] 20 | } 21 | }; 22 | 23 | Patchwork.patch(target, source, patches); 24 | 25 | expect(target.baz).to.have.lengthOf(3); 26 | }); 27 | 28 | it('should patch a static source path to a dynamic target path', function() { 29 | var patches = [{ 30 | target: { 31 | path: '/bar/@' 32 | }, 33 | source: { 34 | path: '/foo' 35 | } 36 | }]; 37 | var target = { 38 | bar: [1, 2, 3] 39 | }; 40 | var source = { 41 | foo: [1, 2, 3] 42 | }; 43 | 44 | Patchwork.patch(target, source, patches); 45 | 46 | expect(target.bar).to.have.lengthOf(3); 47 | expect(target.bar[0]).to.have.lengthOf(3); 48 | expect(target.bar[1]).to.have.lengthOf(3); 49 | expect(target.bar[2]).to.have.lengthOf(3); 50 | }); 51 | 52 | it('should patch a dynamic source path to a static target path', function() { 53 | var source = { 54 | things: [{ 55 | type: 'foo', 56 | about: { 57 | more: [1, 2, 3, 4] 58 | } 59 | }, { 60 | type: 'bar', 61 | about: { 62 | more: [5, 6, 7, 8] 63 | } 64 | }, { 65 | type: 'foo', 66 | about: { 67 | more: [9, 10, 11, 4] 68 | } 69 | }] 70 | }; 71 | var target = {}; 72 | var patches = [{ 73 | collect: true, 74 | unique: true, 75 | target: { 76 | path: '/here' 77 | }, 78 | source: { 79 | path: '/things/@/about/more/@', 80 | tests: [{ 81 | path: '/things/@/type', 82 | operator: '==', 83 | value: 'foo' 84 | }] 85 | }, 86 | operations: [{ 87 | type: 'shape', 88 | shape: { 89 | id: '@' 90 | } 91 | }] 92 | }]; 93 | 94 | Patchwork.patch(target, source, patches); 95 | 96 | expect(target.here).to.have.lengthOf(7); 97 | expect(target.here.length).to.equal(_.uniq(target.here).length); 98 | }); 99 | 100 | it('should patch a dynamic source path to a dynamic target path', function() { 101 | var source = { 102 | stuff: [{ 103 | identifier: { 104 | id: 1 105 | }, 106 | foo: 'hi', 107 | bar: 'bye' 108 | }, { 109 | identifier: { 110 | id: 2 111 | }, 112 | foo: 'bye', 113 | bar: 'hi' 114 | }] 115 | }; 116 | var target = { 117 | things: [{ 118 | about: { 119 | more: [1] 120 | } 121 | }, { 122 | about: { 123 | more: [3, 2, 1] 124 | } 125 | }, { 126 | about: { 127 | more: [2] 128 | } 129 | }] 130 | }; 131 | var patches = [{ 132 | collect: true, 133 | target: { 134 | path: '/things/@/about/here' 135 | }, 136 | source: { 137 | path: '/stuff/@', 138 | tests: [{ 139 | path: '/stuff/@/identifier/id', 140 | operator: '(==)', 141 | value: '/things/@/about/more/@' 142 | }] 143 | }, 144 | operations: [{ 145 | type: 'shape', 146 | shape: { 147 | id: '/identifier/id', 148 | foo: '/foo' 149 | } 150 | }] 151 | }]; 152 | 153 | Patchwork.patch(target, source, patches); 154 | 155 | expect(target.things).to.have.lengthOf(3); 156 | expect(target.things[0].about.here).to.have.lengthOf(1); 157 | expect(target.things[0].about.here[0].foo).to.equal('hi'); 158 | expect(target.things[0].about.here[0].id).to.equal(1); 159 | expect(target.things[0].about.here[0].bar).to.be.undefined; 160 | expect(target.things[1].about.here).to.have.lengthOf(2); 161 | expect(target.things[1].about.here[0].foo).to.equal('hi'); 162 | expect(target.things[1].about.here[0].id).to.equal(1); 163 | expect(target.things[1].about.here[0].bar).to.be.undefined; 164 | expect(target.things[1].about.here[1].foo).to.equal('bye'); 165 | expect(target.things[1].about.here[1].id).to.equal(2); 166 | expect(target.things[1].about.here[1].bar).to.be.undefined; 167 | expect(target.things[2].about.here).to.have.lengthOf(1); 168 | expect(target.things[2].about.here[0].foo).to.equal('bye'); 169 | expect(target.things[2].about.here[0].id).to.equal(2); 170 | expect(target.things[2].about.here[0].bar).to.be.undefined; 171 | }); 172 | 173 | it('should patch a branch from a source document to a target document', function() { 174 | var source = { 175 | foo: { 176 | bar: [{ 177 | something: 1 178 | }], 179 | baz: [{ 180 | something: 2 181 | }] 182 | } 183 | }; 184 | var target = {}; 185 | var patches = [{ 186 | depth: -2, 187 | target: { 188 | path: '/here' 189 | }, 190 | source: { 191 | path: '/foo/@/@' 192 | } 193 | }]; 194 | 195 | Patchwork.patch(target, source, patches); 196 | 197 | expect(target.here.bar).to.have.lengthOf(1) 198 | expect(target.here.bar[0].something).to.equal(1); 199 | expect(target.here.baz).to.have.lengthOf(1) 200 | expect(target.here.baz[0].something).to.equal(2); 201 | }); 202 | }); 203 | -------------------------------------------------------------------------------- /test/specs/common.spec.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var expect = chai.expect; 3 | var common = require('../../common'); 4 | 5 | describe('common.js', function() { 6 | it('#trim: should trim whitespace from a string', function() { 7 | expect(common.trim(' foo ')).to.equal('foo'); 8 | }); 9 | 10 | it('#isRegExp: should validate a RegExp object', function() { 11 | expect(common.isRegExp(new RegExp)).to.be.true; 12 | expect(common.isRegExp(/(?:)/)).to.be.true; 13 | expect(common.isRegExp('foo')).to.be.false; 14 | }); 15 | 16 | it('#isArray: should validate an Array object', function() { 17 | expect(common.isArray(new Array)).to.be.true; 18 | expect(common.isArray([])).to.be.true; 19 | expect(common.isArray('foo')).to.be.false; 20 | }); 21 | 22 | it('#isArrayLike: should validate an Array-like object', function() { 23 | expect(common.isArrayLike({ 24 | 0: 'foo', 25 | length: 1 26 | })).to.be.true; 27 | expect(common.isArrayLike(new Array)).to.be.true; 28 | expect(common.isArrayLike([])).to.be.true; 29 | expect(common.isArrayLike('foo')).to.be.false; 30 | }); 31 | 32 | it('#isPlainObject: should validate a plain object', function() { 33 | expect(common.isPlainObject(new Object)).to.be.true; 34 | expect(common.isPlainObject({})).to.be.true; 35 | expect(common.isPlainObject('foo')).to.be.false; 36 | }); 37 | 38 | it('#isObject: should validate an object', function() { 39 | expect(common.isObject(new Array)).to.be.true; 40 | expect(common.isObject([])).to.be.true; 41 | expect(common.isObject('foo')).to.be.false; 42 | }); 43 | 44 | it('#toArray: should convert to an array', function() { 45 | expect(common.toArray({ 46 | 0: 'foo', 47 | length: 1 48 | })).to.deep.equal(['foo']); 49 | }); 50 | 51 | it('#toInt: should convert to an integer', function() { 52 | expect(common.toInt('1')).to.equal(1); 53 | expect(common.toInt('a', 2)).to.equal(2); 54 | expect(common.toInt('0x10', 16)).to.equal(16); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /test/specs/error.spec.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var expect = chai.expect; 3 | var error = require('../../error'); 4 | 5 | describe('error.js', function() { 6 | it('should instantiate itself', function() { 7 | expect((error())).to.be.an.instanceOf(error); 8 | }); 9 | 10 | it('should have the correct constructor name', function() { 11 | expect((new error()).name).to.equal('PatchworkError'); 12 | }); 13 | 14 | it('should contain the correct message', function() { 15 | expect((new error('foo')).message).to.equal('foo'); 16 | }); 17 | 18 | it('should accept an error as the first argument', function() { 19 | expect((new error(new Error('foo'))).message).to.equal('foo'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/specs/index.spec.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var expect = chai.expect; 3 | var patchwork = require('../../index'); 4 | 5 | describe('index.js', function() { 6 | var source = require('../fixtures/source.json'); 7 | 8 | it('should get a value', function() { 9 | expect(patchwork.get(source, '/')).to.deep.equal(source); 10 | expect(patchwork.get(source, '/foo')).to.deep.equal(source.foo); 11 | }); 12 | 13 | it('should set a value', function() { 14 | expect(patchwork.set({}, '/', { 15 | foo: 1 16 | })).to.deep.equal({ 17 | foo: 1 18 | }); 19 | expect(patchwork.set({}, '/foo', 1)).to.deep.equal({ 20 | foo: 1 21 | }); 22 | }); 23 | 24 | it('should get a value with at a given path', function() { 25 | expect(patchwork.get({ 26 | foo: { 27 | bar: [3, 4] 28 | } 29 | }, '/foo/bar/1')).to.equal(4); 30 | }); 31 | 32 | it('should set a value with at a given path', function() { 33 | var fixture = { 34 | foo: { 35 | bar: [3, 4] 36 | } 37 | }; 38 | 39 | patchwork.set(fixture, '/foo/bar/2', 'baz'); 40 | 41 | expect(fixture.foo.bar[2]).to.equal('baz'); 42 | }); 43 | 44 | it('should return false if invalid patches supplied', function() { 45 | expect(patchwork.patch({}, {}, '')).to.be.false; 46 | }); 47 | 48 | it('should return false there were no side-effects', function() { 49 | var target = {}; 50 | var didPatch = patchwork.patch(target, source, [{ 51 | source: { 52 | path: '/iDontExist' 53 | }, 54 | target: { 55 | path: '/bar' 56 | } 57 | }]); 58 | 59 | expect(didPatch).to.be.false; 60 | expect(target).to.deep.equal({}); 61 | }); 62 | 63 | it('should successfully patch a target', function() { 64 | var target = {}; 65 | var didPatch = patchwork.patch(target, source, [{ 66 | source: { 67 | path: '/foo' 68 | }, 69 | target: { 70 | path: '/bar' 71 | } 72 | }]); 73 | 74 | expect(didPatch).to.be.true; 75 | expect(target).to.deep.equal({ 76 | bar: source.foo 77 | }); 78 | }); 79 | 80 | it('should successfully patch a target with a supplied target test', function() { 81 | var target = { 82 | bar: true 83 | }; 84 | 85 | var didPatch = patchwork.patch(target, source, [{ 86 | source: { 87 | path: '/foo' 88 | }, 89 | target: { 90 | path: '/bar', 91 | tests: [{ 92 | path: '/bar', 93 | operator: '===', 94 | value: false 95 | }] 96 | } 97 | }]); 98 | 99 | expect(didPatch).to.be.false; 100 | expect(target).to.deep.equal({ 101 | bar: true 102 | }); 103 | }); 104 | 105 | it('should successfully patch a target with a supplied source test', function() { 106 | var target = { 107 | bar: true 108 | }; 109 | 110 | var didPatch = patchwork.patch(target, source, [{ 111 | source: { 112 | path: '/foo', 113 | tests: [{ 114 | path: '/foo/qix/0', 115 | operator: '===', 116 | value: 4 117 | }] 118 | }, 119 | target: { 120 | path: '/bar' 121 | } 122 | }]); 123 | 124 | expect(didPatch).to.be.false; 125 | expect(target).to.deep.equal({ 126 | bar: true 127 | }); 128 | }); 129 | 130 | it('should successfully patch a target with supplied trie depth', function() { 131 | var target = {}; 132 | 133 | patchwork.patch(target, source, [{ 134 | depth: -2, 135 | source: { 136 | path: '/foo/baz' 137 | }, 138 | target: { 139 | path: '/bar' 140 | } 141 | }]); 142 | 143 | expect(target).to.deep.equal({ 144 | bar: { 145 | foo: { 146 | baz: 2 147 | } 148 | } 149 | }); 150 | }); 151 | 152 | it('should successfully merge into a target path', function() { 153 | var target = { 154 | foo: { 155 | mergeTest: 0 156 | } 157 | }; 158 | 159 | patchwork.patch(target, source, [{ 160 | merge: true, 161 | source: { 162 | path: '/foo' 163 | }, 164 | target: { 165 | path: '/foo' 166 | } 167 | }]); 168 | 169 | expect(target).to.deep.equal({ 170 | foo: { 171 | mergeTest: 0, 172 | bar: source.foo.bar, 173 | baz: source.foo.baz, 174 | qix: source.foo.qix 175 | } 176 | }); 177 | }); 178 | 179 | it('should successfully merge over a target path', function() { 180 | var target = { 181 | foo: [1, 2, 3, 4, 5, 6] 182 | }; 183 | 184 | patchwork.patch(target, { 185 | foo: [4, 5, 6] 186 | }, [{ 187 | merge: true, 188 | source: { 189 | path: '/foo' 190 | }, 191 | target: { 192 | path: '/foo' 193 | } 194 | }]); 195 | 196 | expect(target).to.deep.equal({ 197 | foo: [4, 5, 6, 4, 5, 6] 198 | }); 199 | }); 200 | 201 | it('should successfully merge over a target path taking only unique values', function() { 202 | var target = { 203 | foo: [1, 2, 3, 4, 5, 6] 204 | }; 205 | 206 | patchwork.patch(target, { 207 | foo: [4, 5, 6] 208 | }, [{ 209 | merge: true, 210 | unique: true, 211 | source: { 212 | path: '/foo' 213 | }, 214 | target: { 215 | path: '/foo' 216 | } 217 | }]); 218 | 219 | expect(target).to.deep.equal({ 220 | foo: [4, 5, 6] 221 | }); 222 | }); 223 | 224 | it('should successfully collect at a target path', function() { 225 | var target = { 226 | foo: { 227 | collectTest: 0 228 | } 229 | }; 230 | 231 | patchwork.patch(target, source, [{ 232 | collect: true, 233 | source: { 234 | path: '/foo' 235 | }, 236 | target: { 237 | path: '/foo' 238 | } 239 | }]); 240 | 241 | expect(target).to.deep.equal({ 242 | foo: [{ 243 | collectTest: 0 244 | }, source.foo] 245 | }); 246 | }); 247 | 248 | it('should successfully collect into a target path', function() { 249 | var target = { 250 | foo: { 251 | collectTest: [0] 252 | } 253 | }; 254 | 255 | patchwork.patch(target, source, [{ 256 | collect: true, 257 | source: { 258 | path: '/foo' 259 | }, 260 | target: { 261 | path: '/foo/collectTest' 262 | } 263 | }]); 264 | 265 | expect(target).to.deep.equal({ 266 | foo: { 267 | collectTest: [0, source.foo] 268 | } 269 | }); 270 | }); 271 | 272 | it('should successfully collect into an undefined target path', function() { 273 | var target = { 274 | foo: {} 275 | }; 276 | 277 | patchwork.patch(target, source, [{ 278 | collect: true, 279 | source: { 280 | path: '/foo' 281 | }, 282 | target: { 283 | path: '/foo/collectTest' 284 | } 285 | }]); 286 | 287 | expect(target).to.deep.equal({ 288 | foo: { 289 | collectTest: [source.foo] 290 | } 291 | }); 292 | }); 293 | 294 | it('should successfully collect into an undefined target path (source array)', function() { 295 | var target = { 296 | foo: {} 297 | }; 298 | 299 | patchwork.patch(target, source, [{ 300 | collect: true, 301 | source: { 302 | path: '/foo/qix' 303 | }, 304 | target: { 305 | path: '/foo/collectTest' 306 | } 307 | }]); 308 | 309 | expect(target).to.deep.equal({ 310 | foo: { 311 | collectTest: source.foo.qix 312 | } 313 | }); 314 | }); 315 | 316 | it('should successfully patch a root target path (primitive)', function() { 317 | var target = 'foo'; 318 | 319 | patchwork.patch(target, { 320 | foo: [{ 321 | bar: 1 322 | }, { 323 | bar: 2 324 | }] 325 | }, [{ 326 | collect: true, 327 | source: { 328 | path: '/foo/@' 329 | }, 330 | target: { 331 | path: '/' 332 | } 333 | }]); 334 | 335 | expect(target).to.deep.equal('foo'); 336 | }); 337 | 338 | it('should successfully patch a root target array path (collect)', function() { 339 | var target = []; 340 | 341 | patchwork.patch(target, { 342 | foo: [{ 343 | bar: 1 344 | }, { 345 | bar: 2 346 | }] 347 | }, [{ 348 | collect: true, 349 | source: { 350 | path: '/foo/@' 351 | }, 352 | target: { 353 | path: '/' 354 | } 355 | }]); 356 | 357 | expect(target).to.deep.equal([{ 358 | bar: 1 359 | }, { 360 | bar: 2 361 | }]); 362 | }); 363 | 364 | it('should successfully patch a root object path (collect)', function() { 365 | var target = {}; 366 | 367 | patchwork.patch(target, { 368 | foo: [{ 369 | bar: 1 370 | }, { 371 | bar: 2 372 | }] 373 | }, [{ 374 | collect: true, 375 | source: { 376 | path: '/foo/@' 377 | }, 378 | target: { 379 | path: '/' 380 | } 381 | }]); 382 | 383 | expect(target).to.deep.equal({ 384 | 0: { 385 | bar: 1 386 | }, 387 | 1: { 388 | bar: 2 389 | }, 390 | length: 2 391 | }); 392 | }); 393 | 394 | it('should successfully patch a root target path (merge)', function() { 395 | var target1 = { 396 | iShouldGetDeleted: false 397 | }; 398 | 399 | var target2 = { 400 | iShouldGetDeleted: true 401 | }; 402 | 403 | [target1, target2].forEach(function(target, idx) { 404 | patchwork.patch(target, { 405 | foo: [{ 406 | bar: 1 407 | }, { 408 | bar: 2 409 | }] 410 | }, [{ 411 | merge: idx === 0, 412 | source: { 413 | path: '/foo/1' 414 | }, 415 | target: { 416 | path: '/' 417 | } 418 | }]); 419 | }); 420 | 421 | expect(target1).to.deep.equal({ 422 | bar: 2, 423 | iShouldGetDeleted: false 424 | }); 425 | 426 | expect(target2).to.deep.equal({ 427 | bar: 2 428 | }); 429 | }); 430 | 431 | it('should successfully patch from a root source path (primitive) into a target (object, collect)', function() { 432 | var source = 'foo'; 433 | var target = {}; 434 | 435 | patchwork.patch(target, source, [{ 436 | collect: true, 437 | source: { 438 | path: '/' 439 | }, 440 | target: { 441 | path: '/' 442 | } 443 | }]); 444 | 445 | expect(target[0]).to.equal('foo'); 446 | expect(target.length).to.equal(1); 447 | }); 448 | 449 | it('should successfully patch from a root source path (primitive) into a target (object)', function() { 450 | var source = 'foo'; 451 | var target = {}; 452 | 453 | patchwork.patch(target, source, [{ 454 | source: { 455 | path: '/' 456 | }, 457 | target: { 458 | path: '/' 459 | } 460 | }]); 461 | 462 | expect(target[0]).to.equal('f'); 463 | expect(target[1]).to.equal('o'); 464 | expect(target[2]).to.equal('o'); 465 | expect(target.length).to.be.undefined; 466 | }); 467 | 468 | it('should successfully patch from a root source path (primitive) into a target (array, collect)', function() { 469 | var source = 'foo'; 470 | var target = []; 471 | 472 | patchwork.patch(target, source, [{ 473 | collect: true, 474 | source: { 475 | path: '/' 476 | }, 477 | target: { 478 | path: '/' 479 | } 480 | }]); 481 | 482 | expect(target[0]).to.equal('foo'); 483 | expect(target.length).to.equal(1); 484 | }); 485 | 486 | it('should successfully patch from a root source path (primitive) into a target (array)', function() { 487 | var source = 'foo'; 488 | var target = []; 489 | 490 | patchwork.patch(target, source, [{ 491 | source: { 492 | path: '/' 493 | }, 494 | target: { 495 | path: '/' 496 | } 497 | }]); 498 | 499 | expect(target[0]).to.equal('foo'); 500 | expect(target.length).to.equal(1); 501 | }); 502 | 503 | it('should successfully patch from a root source array path (collect)', function() { 504 | var source = ['foo']; 505 | var target = { 506 | foo: [{ 507 | bar: 1 508 | }, { 509 | bar: 2 510 | }] 511 | }; 512 | 513 | patchwork.patch(target, source, [{ 514 | collect: true, 515 | source: { 516 | path: '/' 517 | }, 518 | target: { 519 | path: '/foo/@' 520 | } 521 | }]); 522 | 523 | expect(target).to.deep.equal({ 524 | foo: [ [ { bar: 1 }, 'foo' ], [ { bar: 2 }, 'foo' ] ] 525 | }); 526 | }); 527 | 528 | it('should successfully patch from a root source array path', function() { 529 | var source = ['foo']; 530 | var target = { 531 | foo: [{ 532 | bar: 1 533 | }, { 534 | bar: 2 535 | }] 536 | }; 537 | 538 | patchwork.patch(target, source, [{ 539 | source: { 540 | path: '/' 541 | }, 542 | target: { 543 | path: '/foo/@' 544 | } 545 | }]); 546 | 547 | expect(target).to.deep.equal({ 548 | foo: [ ['foo'], ['foo'] ] 549 | }); 550 | }); 551 | 552 | it('should successfully patch a root object path', function() { 553 | var target = { bar: 2, baz: 3 }; 554 | var source = { foo: 1 }; 555 | 556 | patchwork.patch(target, source, [{ 557 | source: { 558 | path: '/' 559 | }, 560 | target: { 561 | path: '/' 562 | } 563 | }]); 564 | 565 | expect(target).to.deep.equal({ 566 | foo: 1 567 | }); 568 | }); 569 | 570 | 571 | it('should successfully patch a root object path (collect)', function() { 572 | var target = { bar: 2, baz: 3 }; 573 | var source = { foo: 1 }; 574 | 575 | patchwork.patch(target, source, [{ 576 | collect: true, 577 | source: { 578 | path: '/' 579 | }, 580 | target: { 581 | path: '/' 582 | } 583 | }]); 584 | 585 | expect(target).to.deep.equal({ 586 | '0': { foo: 1 }, length: 1 587 | }); 588 | }); 589 | 590 | 591 | it('should successfully patch a root object path (merge)', function() { 592 | var target = { bar: 2, baz: 3 }; 593 | var source = { foo: 1 }; 594 | 595 | patchwork.patch(target, source, [{ 596 | merge: true, 597 | source: { 598 | path: '/' 599 | }, 600 | target: { 601 | path: '/' 602 | } 603 | }]); 604 | 605 | expect(target).to.deep.equal({ 606 | bar: 2, baz: 3, foo: 1 607 | }); 608 | }); 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | it('should successfully patch with an operation', function() { 619 | var target = {}; 620 | var called = 0; 621 | var fooBarOperation = function(operation, source) { 622 | called++; 623 | 624 | return { 625 | foo: 'bar' 626 | }; 627 | }; 628 | 629 | patchwork.register('fooBar', fooBarOperation); 630 | 631 | patchwork.patch(target, source, [{ 632 | source: { 633 | path: '/foo' 634 | }, 635 | target: { 636 | path: '/foo' 637 | }, 638 | operations: [{ 639 | type: 'fooBar' 640 | }] 641 | }]); 642 | 643 | expect(called).to.equal(1); 644 | expect(target).to.deep.equal({ 645 | foo: { 646 | foo: 'bar' 647 | } 648 | }); 649 | 650 | patchwork.unregister('fooBar'); 651 | }); 652 | 653 | it('should log patches', function() { 654 | var log = []; 655 | var target = {}; 656 | var patches = [{ 657 | source: { 658 | path: '/foo' 659 | }, 660 | target: { 661 | path: '/bar' 662 | } 663 | }]; 664 | 665 | patchwork.patch(target, source, patches, log); 666 | 667 | expect(log).to.have.lengthOf(1); 668 | }); 669 | }); 670 | -------------------------------------------------------------------------------- /test/specs/operations/index.spec.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var expect = chai.expect; 3 | 4 | describe('operations/index.js', function() { 5 | var modulePath = '../../../operations'; 6 | var operations; 7 | 8 | beforeEach(function() { 9 | operations = req(modulePath); 10 | }); 11 | 12 | after(function() { 13 | unreq(modulePath); 14 | }); 15 | 16 | it('should have all core operations', function() { 17 | expect(operations.registered('shape')).to.be.true; 18 | }); 19 | 20 | it('should successfully register an operation', function() { 21 | operations.register('foo', function() {}); 22 | expect(operations.registered('foo')).to.be.true; 23 | }); 24 | 25 | it('should successfully unregister an operation', function() { 26 | operations.register('foo', function() {}); 27 | operations.unregister('foo'); 28 | expect(operations.registered('foo')).to.be.false; 29 | }); 30 | 31 | it('should fail to unregister an operation', function() { 32 | expect(function() { 33 | operations.unregister('bar'); 34 | }).to.throw(Error); 35 | }); 36 | 37 | it('should execute a core operation', function() { 38 | var source = { 39 | foo: { 40 | bar: { 41 | baz: 1 42 | } 43 | } 44 | }; 45 | 46 | var result = operations.execute({ 47 | type: 'shape', 48 | shape: { 49 | qix: '/foo/bar/baz' 50 | } 51 | }, source); 52 | 53 | expect(result).to.deep.equal({ 54 | qix: 1 55 | }); 56 | }); 57 | 58 | it('should execute a local operation', function() { 59 | operations.register('foo', function() { 60 | return 'foo'; 61 | }); 62 | 63 | expect(operations.execute({ 64 | type: 'foo' 65 | })).to.equal('foo'); 66 | }); 67 | 68 | it('should fail to execute an unregistered operation', function() { 69 | expect(function() { 70 | operations.execute({ 71 | type: 'foo' 72 | }); 73 | }).to.throw(Error); 74 | }); 75 | }); 76 | 77 | function req(path) { 78 | unreq(path); 79 | return require(path); 80 | } 81 | 82 | function unreq(path) { 83 | delete require.cache[require.resolve(path)]; 84 | } 85 | -------------------------------------------------------------------------------- /test/specs/operations/shape.spec.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var expect = chai.expect; 3 | 4 | describe('operations/shape.js', function() { 5 | var source = require('../../fixtures/source.json'); 6 | var shape; 7 | 8 | beforeEach(function() { 9 | shape = require('../../../operations/shape'); 10 | }); 11 | 12 | it('should shape an object', function() { 13 | var result = shape({ 14 | type: 'shape', 15 | shape: { 16 | qix: { 17 | qux: '/foo/bar' 18 | } 19 | } 20 | }, source); 21 | 22 | expect(result).to.deep.equal({ 23 | qix: { 24 | qux: 1 25 | } 26 | }); 27 | }); 28 | 29 | it('should shape an object with defaults', function() { 30 | var result = shape({ 31 | type: 'shape', 32 | shape: { 33 | qix: { 34 | qux: ['/foo/bar/baz', 2] 35 | } 36 | } 37 | }, source); 38 | 39 | expect(result).to.deep.equal({ 40 | qix: { 41 | qux: 2 42 | } 43 | }); 44 | }); 45 | 46 | it('should shape an object with defaults with single token', function() { 47 | var result = shape({ 48 | type: 'shape', 49 | shape: { 50 | qix: { 51 | qux: ['/foo/bar/baz', '<%/foo/bar%>'] 52 | } 53 | } 54 | }, source); 55 | 56 | expect(result).to.deep.equal({ 57 | qix: { 58 | qux: '1' 59 | } 60 | }); 61 | }); 62 | 63 | it('should shape an object with defaults with multiple tokens', function() { 64 | var result = shape({ 65 | type: 'shape', 66 | shape: { 67 | qix: { 68 | qux1: ['/foo/bar/baz', 'test:<%/foo/bar%>:<%/foo/baz%>'], 69 | qux2: ['/foo/bar/baz', '<%/foo/bar%>:<%/foo/baz%>'] 70 | 71 | } 72 | } 73 | }, source); 74 | 75 | expect(result).to.deep.equal({ 76 | qix: { 77 | qux1: 'test:1:2', 78 | qux2: '1:2' 79 | } 80 | }); 81 | }); 82 | 83 | it('should shape an object with source value reference', function() { 84 | var result = shape({ 85 | type: 'shape', 86 | shape: { 87 | qix: { 88 | qux: '@' 89 | } 90 | } 91 | }, source); 92 | 93 | expect(result).to.deep.equal({ 94 | qix: { 95 | qux: source 96 | } 97 | }); 98 | }); 99 | 100 | it('should shape an object with a dynamic path', function() { 101 | var result = shape({ 102 | type: 'shape', 103 | shape: { 104 | qix: { 105 | qux: '/foo/@' 106 | } 107 | } 108 | }, source); 109 | 110 | expect(result).to.deep.equal({ 111 | qix: { 112 | qux: [1, 2, [3, 4]] 113 | } 114 | }); 115 | }); 116 | 117 | it('should shape an object with virtuals', function() { 118 | var result = shape({ 119 | type: 'shape', 120 | virtual: { 121 | virtualTest: [{ 122 | target: { 123 | path: '/virtualTest' 124 | }, 125 | source: { 126 | path: '/hello/world' 127 | } 128 | }] 129 | }, 130 | shape: { 131 | qix: { 132 | qux: '$virtualTest' 133 | } 134 | } 135 | }, source); 136 | 137 | expect(result).to.deep.equal({ 138 | qix: { 139 | qux: 'lorem' 140 | } 141 | }); 142 | }); 143 | 144 | it('should shape an object with virtuals and defaults', function() { 145 | var result = shape({ 146 | type: 'shape', 147 | virtual: { 148 | virtualTest: [{ 149 | target: { 150 | path: '/virtualTest' 151 | }, 152 | source: { 153 | path: '/hello/world/foo' 154 | } 155 | }] 156 | }, 157 | shape: { 158 | qix: { 159 | qux: ['$virtualTest', 'virtual-default'] 160 | } 161 | } 162 | }, source); 163 | 164 | expect(result).to.deep.equal({ 165 | qix: { 166 | qux: 'virtual-default' 167 | } 168 | }); 169 | }); 170 | }); 171 | -------------------------------------------------------------------------------- /test/specs/path/expand.spec.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var expect = chai.expect; 3 | 4 | describe('path/expand.js', function() { 5 | var source = require('../../fixtures/source.json'); 6 | var expand; 7 | 8 | beforeEach(function() { 9 | expand = require('../../../path/expand'); 10 | }); 11 | 12 | it('should return a root path', function() { 13 | expect(expand(source.foo, '/')).to.deep.equal(['/']); 14 | }); 15 | 16 | it('should return a single path', function() { 17 | expect(expand(source.foo, '/nonExistentPath')).to.deep.equal(['/nonExistentPath']); 18 | }); 19 | 20 | it('should return an empty array (asArray)', function() { 21 | expect(expand(source.foo, '/nonExistentPath', false, true)).to.deep.equal([['nonExistentPath']]); 22 | }); 23 | 24 | it('should return an empty array (strict + asArray)', function() { 25 | expect(expand(source.foo, '/nonExistentPath', true, true)).to.be.empty; 26 | }); 27 | 28 | it('should expand into an object', function() { 29 | expect(expand(source, '/foo/@')).to.deep.equal([ 30 | '/foo/bar', 31 | '/foo/baz', 32 | '/foo/qix' 33 | ]); 34 | }); 35 | 36 | it('should expand into an array', function() { 37 | expect(expand(source, '/foo/qix/@')).to.deep.equal([ 38 | '/foo/qix/0', 39 | '/foo/qix/1' 40 | ]); 41 | }); 42 | 43 | it('should expand into a root array', function() { 44 | expect(expand(source.foo.qix, '/@')).to.deep.equal([ 45 | '/0', 46 | '/1' 47 | ]); 48 | }); 49 | 50 | it('should expand into a root object', function() { 51 | expect(expand(source.foo, '/@')).to.deep.equal([ 52 | '/bar', 53 | '/baz', 54 | '/qix' 55 | ]); 56 | }); 57 | 58 | it('should accept an array as a path', function() { 59 | expect(expand(source, ['@', 'baz'], true, true)).to.deep.equal([ 60 | ['foo', 'baz'] 61 | ]); 62 | }); 63 | 64 | it('should return an array of paths without expansion', function() { 65 | expect(expand(source, '/foo')).to.deep.equal(['/foo']); 66 | }); 67 | 68 | it('should return an array of paths without expansion (strict)', function() { 69 | expect(expand(source, '/foo', true)).to.deep.equal(['/foo']); 70 | }); 71 | 72 | it('should return an array of paths without expansion (exploded)', function() { 73 | expect(expand(source, '/foo', false, true)).to.deep.equal([ 74 | ['foo'] 75 | ]); 76 | }); 77 | 78 | it('should return an array of paths without expansion (strict + exploded)', function() { 79 | expect(expand(source, '/foo', true, true)).to.deep.equal([ 80 | ['foo'] 81 | ]); 82 | }); 83 | 84 | it('should return an array of expanded paths', function() { 85 | expect(expand(source, '/@')).to.deep.equal(['/foo', '/hello']); 86 | }); 87 | 88 | it('should return an array of expanded paths (strict)', function() { 89 | expect(expand(source, '/@', true)).to.deep.equal(['/foo', '/hello']); 90 | }); 91 | 92 | it('should return an array of expanded paths (exploded)', function() { 93 | expect(expand(source, '/@', false, true)).to.deep.equal([ 94 | ['foo'], 95 | ['hello'] 96 | ]); 97 | }); 98 | 99 | it('should return an array of expanded paths (strict + exploded)', function() { 100 | expect(expand(source, '/@', true, true)).to.deep.equal([ 101 | ['foo'], 102 | ['hello'] 103 | ]); 104 | }); 105 | 106 | it('should return only paths that exist', function() { 107 | expect(expand(source, '/@/baz', true)).to.deep.equal(['/foo/baz']); 108 | }); 109 | 110 | it('should return only paths that exist (exploded)', function() { 111 | expect(expand(source, '/@/baz', true, true)).to.deep.equal([ 112 | ['foo', 'baz'] 113 | ]); 114 | }); 115 | 116 | it('should expand over primitive values', function() { 117 | expect(expand(source, '/foo/@/@')).to.deep.equal([ 118 | '/foo/qix/0', 119 | '/foo/qix/1' 120 | ]); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /test/specs/path/intersect.spec.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var expect = chai.expect; 3 | 4 | describe('path/intersect.js', function() { 5 | var source = require('../../fixtures/source.json'); 6 | var intersect; 7 | 8 | beforeEach(function() { 9 | intersect = require('../../../path/intersect'); 10 | }); 11 | 12 | it('should return left-hand path with intersected parts', function() { 13 | var result = intersect( 14 | '/foo/0/bar/baz', 15 | '/foo/@/bar/qix/0', 16 | '/foo/@/bar/qix/@' 17 | ); 18 | 19 | expect(result).to.deep.equal(['foo', '0', 'bar', 'baz', '0']); 20 | }); 21 | 22 | it('should return left-hand path without intersection', function() { 23 | var result = intersect( 24 | '/bar/qix/0', 25 | '/bar/qix/0', 26 | '/foo/@/bar/qix/@' 27 | ); 28 | 29 | expect(result).to.deep.equal(['bar', 'qix', '0']); 30 | }); 31 | 32 | it('should return left-hand path with intersected parts (array-path arguments)', function() { 33 | var result = intersect( 34 | ['foo', '0', 'bar', 'baz'], 35 | ['foo', '@', 'bar', 'qix', '0'], 36 | ['foo', '@', 'bar', 'qix', '@'] 37 | ); 38 | 39 | expect(result).to.deep.equal(['foo', '0', 'bar', 'baz', '0']); 40 | }); 41 | 42 | it('should return left-hand path without intersection (array-path arguments)', function() { 43 | var result = intersect( 44 | ['bar', 'qix', '0'], 45 | ['bar', 'qix', '0'], 46 | ['foo', '@', 'bar', 'qix', '@'] 47 | ); 48 | 49 | expect(result).to.deep.equal(['bar', 'qix', '0']); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/specs/path/split.spec.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var expect = chai.expect; 3 | var split = require('../../../path/split'); 4 | 5 | describe('path/split.js', function() { 6 | it('should split a path', function() { 7 | expect(split('/foo/bar')).to.deep.equal(['foo', 'bar']); 8 | }); 9 | 10 | it('should ignore escaped path delimiters', function() { 11 | expect(split('/foo\\/bar/baz')).to.deep.equal(['foo/bar', 'baz']); 12 | }); 13 | 14 | it('should filter empty path parts', function() { 15 | expect(split('/foo///bar')).to.deep.equal(['foo', 'bar']); 16 | }); 17 | 18 | it('should return an empty array', function() { 19 | expect(split('///')).to.have.lengthOf(0); 20 | }); 21 | 22 | it('should accept an array path', function() { 23 | expect(split(['foo', 'bar'])).to.deep.equal(['foo', 'bar']); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/specs/path/test.spec.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var expect = chai.expect; 3 | 4 | describe('path/test.js', function() { 5 | var test; 6 | 7 | beforeEach(function() { 8 | test = require('../../../path/test'); 9 | }); 10 | 11 | it('should return true with no tests', function() { 12 | expect(test([], {}, {}, '/', '/', '/', '/')).to.be.true; 13 | }); 14 | 15 | it('should accept a single test object', function() { 16 | var tests = { 17 | path: '/foo/0', 18 | operator: '==', 19 | value: '1' 20 | }; 21 | 22 | var source = { 23 | foo: [1] 24 | }; 25 | 26 | expect(test(tests, source, {}, '/foo/0', undefined, '/foo/0', undefined)).to.be.true; 27 | }); 28 | 29 | it('should support dynamic source paths', function() { 30 | var tests = [{ 31 | path: '/foo/@', 32 | operator: '==', 33 | value: '1' 34 | }]; 35 | 36 | var source = { 37 | foo: { 38 | baz: 1 39 | } 40 | }; 41 | 42 | expect(test(tests, source, {}, '/foo/baz', undefined, '/foo/@', undefined)).to.be.true; 43 | }); 44 | 45 | it('should support target operators', function() { 46 | var tests = [{ 47 | path: '/foo', 48 | operator: '(==)', 49 | value: '/bar' 50 | }]; 51 | 52 | var source = { 53 | foo: 1 54 | }; 55 | 56 | var target = { 57 | bar: 1 58 | }; 59 | 60 | expect(test(tests, source, target, '/foo', '/bar', '/foo', '/bar')).to.be.true; 61 | }); 62 | 63 | it('should accept nested tests', function() { 64 | var tests = [ 65 | { 66 | path: '/foo/0', 67 | operator: '==', 68 | value: '1' 69 | }, 70 | { 71 | path: '/foo/1', 72 | operator: '==', 73 | value: '2' 74 | }, 75 | [{ 76 | path: '/foo/2', 77 | operator: '==', 78 | value: 'a' 79 | }, { 80 | path: '/foo/2', 81 | operator: '==', 82 | value: 'b' 83 | }] 84 | ]; 85 | 86 | var source = { 87 | foo: [1, 2, 'b'] 88 | }; 89 | 90 | expect(test(tests, source, {}, '/foo/0', undefined, '/foo/@', undefined)).to.be.true; 91 | expect(test(tests, source, {}, '/foo/1', undefined, '/foo/@', undefined)).to.be.true; 92 | expect(test(tests, source, {}, '/foo/2', undefined, '/foo/@', undefined)).to.be.true; 93 | }); 94 | 95 | it('should expand source paths for a given test', function() { 96 | var tests = [ 97 | { 98 | path: '/foo/0', 99 | operator: '==', 100 | value: '1' 101 | }, 102 | { 103 | path: '/foo/1', 104 | operator: '==', 105 | value: '2' 106 | }, 107 | [{ 108 | path: '/foo/2', 109 | operator: '==', 110 | value: 'a' 111 | }, { 112 | path: '/foo/2', 113 | operator: '==', 114 | value: 'b' 115 | }] 116 | ]; 117 | 118 | var source = { 119 | foo: [1, 2, 'b'] 120 | }; 121 | 122 | expect(test(tests, source, {}, '/@/@', undefined, '/foo/@', undefined)).to.be.true; 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /test/specs/support/operator.spec.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var expect = chai.expect; 3 | 4 | describe('support/operator.js', function() { 5 | var source = require('../../fixtures/source.json'); 6 | var operator; 7 | 8 | beforeEach(function() { 9 | operator = require('../../../support/operator'); 10 | }); 11 | 12 | it('should throw on an invalid operator', function() { 13 | expect(function() { 14 | operator('foo'); 15 | }).to.throw(Error); 16 | }); 17 | 18 | it('should return operator type', function() { 19 | expect(operator('==').getType()).to.equal('=='); 20 | }); 21 | 22 | it('should return path flag', function() { 23 | expect(operator('==').isPath()).to.be.false; 24 | expect(operator('(==)').isPath()).to.be.true; 25 | }); 26 | 27 | it('should support equality operator [==]', function() { 28 | var compare = operator('=='); 29 | 30 | expect(compare('1', '1')).to.be.true; 31 | expect(compare('1', 1)).to.be.true; 32 | }); 33 | 34 | it('should support inequality operator [!=]', function() { 35 | var compare = operator('!='); 36 | 37 | expect(compare('1', '1')).to.be.false; 38 | expect(compare('1', 1)).to.be.false; 39 | }); 40 | 41 | it('should support strict equality operator [===]', function() { 42 | var compare = operator('==='); 43 | 44 | expect(compare('1', '1')).to.be.true; 45 | expect(compare('1', 1)).to.be.false; 46 | }); 47 | 48 | it('should support strict inequality operator [!==]', function() { 49 | var compare = operator('!=='); 50 | 51 | expect(compare('1', '1')).to.be.false; 52 | expect(compare('1', 1)).to.be.true; 53 | }); 54 | 55 | it('should support regex operator [~]', function() { 56 | var compare = operator('~'); 57 | 58 | expect(compare(1, /\d/)).to.be.true; 59 | expect(compare(1, '\\d')).to.be.true; 60 | expect(compare('a', '\\d')).to.be.false; 61 | }); 62 | 63 | it('should support regex operator [!~]', function() { 64 | var compare = operator('!~'); 65 | 66 | expect(compare(1, /\d/)).to.be.false; 67 | expect(compare(1, '\\d')).to.be.false; 68 | expect(compare('a', '\\d')).to.be.true; 69 | }); 70 | 71 | it('should support regex operator [in]', function() { 72 | var compare = operator('in'); 73 | 74 | expect(compare(1, [1, 2, 3])).to.be.true; 75 | expect(compare(4, [1, 2, 3])).to.be.false; 76 | }); 77 | 78 | it('should support regex operator [notIn]', function() { 79 | var compare = operator('notIn'); 80 | 81 | expect(compare(1, [1, 2, 3])).to.be.false; 82 | expect(compare(4, [1, 2, 3])).to.be.true; 83 | }); 84 | }); 85 | --------------------------------------------------------------------------------