Add JSON Patch functions and immutability helpers

This commit is contained in:
josdejong 2020-04-26 21:33:34 +02:00
parent 1bc28041d3
commit 635616ac1c
18 changed files with 1796 additions and 367 deletions

View File

@ -1,3 +0,0 @@
{
"test": "./src/**/*.test.js"
}

555
package-lock.json generated
View File

@ -139,9 +139,9 @@
"dev": true
},
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
"dev": true
},
"ansi-styles": {
@ -172,12 +172,6 @@
"sprintf-js": "~1.0.2"
}
},
"assertion-error": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
"integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
"dev": true
},
"async-limiter": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
@ -239,20 +233,6 @@
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"dev": true
},
"chai": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz",
"integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==",
"dev": true,
"requires": {
"assertion-error": "^1.1.0",
"check-error": "^1.0.2",
"deep-eql": "^3.0.1",
"get-func-name": "^2.0.0",
"pathval": "^1.1.0",
"type-detect": "^4.0.5"
}
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@ -264,12 +244,6 @@
"supports-color": "^5.3.0"
}
},
"check-error": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
"integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
"dev": true
},
"chokidar": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.1.tgz",
@ -286,6 +260,45 @@
"readdirp": "~3.3.0"
}
},
"cliui": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
"dev": true,
"requires": {
"string-width": "^3.1.0",
"strip-ansi": "^5.2.0",
"wrap-ansi": "^5.1.0"
},
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true
},
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"dev": true,
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0"
}
},
"strip-ansi": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dev": true,
"requires": {
"ansi-regex": "^4.1.0"
}
}
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@ -325,21 +338,21 @@
"integrity": "sha512-pMD+MVR538ipqkG5JXeOEbKWS5um1H4LUUccUQG68qpeqBYbzYy79Gh55jkd2TtPdRfUaLWdv6LPP//5Zt0aPQ==",
"dev": true
},
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
},
"decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
"dev": true
},
"deep-eql": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
"integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
"dev": true,
"requires": {
"type-detect": "^4.0.0"
}
},
"define-properties": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
@ -361,6 +374,12 @@
"integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
"dev": true
},
"emoji-regex": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
"integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
"dev": true
},
"es-abstract": {
"version": "1.17.5",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz",
@ -418,6 +437,15 @@
"to-regex-range": "^5.0.1"
}
},
"find-up": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
"dev": true,
"requires": {
"locate-path": "^3.0.0"
}
},
"flat": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz",
@ -425,14 +453,6 @@
"dev": true,
"requires": {
"is-buffer": "~2.0.3"
},
"dependencies": {
"is-buffer": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
"integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==",
"dev": true
}
}
},
"fs.realpath": {
@ -460,12 +480,6 @@
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true
},
"get-func-name": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
"integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
"dev": true
},
"get-port": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz",
@ -553,6 +567,12 @@
"binary-extensions": "^2.0.0"
}
},
"is-buffer": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
"integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==",
"dev": true
},
"is-callable": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz",
@ -571,6 +591,12 @@
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
"dev": true
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
},
"is-glob": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
@ -692,11 +718,25 @@
"integrity": "sha512-ykt2pgN0aqIy6KQC1CqdWTWkmUwNgaOS6dcpHVjyBJONA+Xi7AtSB1vuxC/U/0tjIP3wcRudwQk1YYzUvzk2bA==",
"dev": true
},
"locate-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
"dev": true,
"requires": {
"p-locate": "^3.0.0",
"path-exists": "^3.0.0"
}
},
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
"dev": true
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
},
"lodash-es": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz",
"integrity": "sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ=="
},
"log-symbols": {
"version": "3.0.0",
@ -760,18 +800,18 @@
"dev": true
},
"mkdirp": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz",
"integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==",
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
},
"mocha": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-7.1.1.tgz",
"integrity": "sha512-3qQsu3ijNS3GkWcccT5Zw0hf/rWvu1fTN9sPvEd81hlwsr30GX2GcDSSoBxo24IR8FelmrAydGC6/1J5QQP4WA==",
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-7.1.2.tgz",
"integrity": "sha512-o96kdRKMKI3E8U0bjnfqW4QMk12MwZ4mhdBTf+B5a1q9+aq2HRnj+3ZdJu0B/ZhJeK78MgYuv6L8d/rA5AeBJA==",
"dev": true,
"requires": {
"ansi-colors": "3.2.3",
@ -787,7 +827,7 @@
"js-yaml": "3.13.1",
"log-symbols": "3.0.0",
"minimatch": "3.0.4",
"mkdirp": "0.5.3",
"mkdirp": "0.5.5",
"ms": "2.1.1",
"node-environment-flags": "1.0.6",
"object.assign": "4.1.0",
@ -816,41 +856,6 @@
"readdirp": "~3.2.0"
}
},
"cliui": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
"dev": true,
"requires": {
"string-width": "^3.1.0",
"strip-ansi": "^5.2.0",
"wrap-ansi": "^5.1.0"
}
},
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
},
"emoji-regex": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
"integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
"dev": true
},
"find-up": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
"dev": true,
"requires": {
"locate-path": "^3.0.0"
}
},
"glob": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
@ -865,43 +870,6 @@
"path-is-absolute": "^1.0.0"
}
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
},
"locate-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
"dev": true,
"requires": {
"p-locate": "^3.0.0",
"path-exists": "^3.0.0"
}
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
"dev": true
},
"p-locate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
"dev": true,
"requires": {
"p-limit": "^2.0.0"
}
},
"path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
"dev": true
},
"readdirp": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz",
@ -911,26 +879,6 @@
"picomatch": "^2.0.4"
}
},
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"dev": true,
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0"
}
},
"strip-ansi": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dev": true,
"requires": {
"ansi-regex": "^4.1.0"
}
},
"supports-color": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz",
@ -939,54 +887,6 @@
"requires": {
"has-flag": "^3.0.0"
}
},
"which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
"dev": true,
"requires": {
"isexe": "^2.0.0"
}
},
"wrap-ansi": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.0",
"string-width": "^3.0.0",
"strip-ansi": "^5.0.0"
}
},
"yargs": {
"version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
"integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
"dev": true,
"requires": {
"cliui": "^5.0.0",
"find-up": "^3.0.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^13.1.2"
}
},
"yargs-parser": {
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
"integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
"dev": true,
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
}
}
},
@ -996,6 +896,12 @@
"integrity": "sha512-d2RKzMD4JNyHMbnbWnznPaa8vbdlq/4pNZ3IgdaGrVbBhebBsGUUE/6qorTMYNS6TwuH3ilfOlD2bf4Igh8CKg==",
"dev": true
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
"dev": true
},
"node-environment-flags": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz",
@ -1004,14 +910,6 @@
"requires": {
"object.getownpropertydescriptors": "^2.0.3",
"semver": "^5.7.0"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
}
}
},
"normalize-path": {
@ -1078,12 +976,27 @@
"p-try": "^2.0.0"
}
},
"p-locate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
"dev": true,
"requires": {
"p-limit": "^2.0.0"
}
},
"p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true
},
"path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
"dev": true
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@ -1096,12 +1009,6 @@
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true
},
"pathval": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz",
"integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=",
"dev": true
},
"picomatch": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
@ -1221,6 +1128,12 @@
"chokidar": ">=2.0.0 <4.0.0"
}
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
},
"serialize-javascript": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz",
@ -1286,6 +1199,16 @@
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
"dev": true
},
"string-width": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
"dev": true,
"requires": {
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^4.0.0"
}
},
"string.prototype.trimend": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz",
@ -1328,6 +1251,15 @@
"es-abstract": "^1.17.5"
}
},
"strip-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
"dev": true,
"requires": {
"ansi-regex": "^3.0.0"
}
},
"strip-indent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
@ -1403,11 +1335,14 @@
"is-number": "^7.0.0"
}
},
"type-detect": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
"dev": true
"which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
"dev": true,
"requires": {
"isexe": "^2.0.0"
}
},
"which-module": {
"version": "2.0.0",
@ -1422,37 +1357,43 @@
"dev": true,
"requires": {
"string-width": "^1.0.2 || 2"
}
},
"wrap-ansi": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.0",
"string-width": "^3.0.0",
"strip-ansi": "^5.0.0"
},
"dependencies": {
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
"dev": true
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true
},
"string-width": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"dev": true,
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^4.0.0"
"strip-ansi": "^5.1.0"
}
},
"strip-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dev": true,
"requires": {
"ansi-regex": "^3.0.0"
"ansi-regex": "^4.1.0"
}
}
}
@ -1478,72 +1419,28 @@
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
"dev": true
},
"yargs-unparser": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz",
"integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==",
"yargs": {
"version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
"integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
"dev": true,
"requires": {
"flat": "^4.1.0",
"lodash": "^4.17.15",
"yargs": "^13.3.0"
"cliui": "^5.0.0",
"find-up": "^3.0.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^13.1.2"
},
"dependencies": {
"cliui": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
"dev": true,
"requires": {
"string-width": "^3.1.0",
"strip-ansi": "^5.2.0",
"wrap-ansi": "^5.1.0"
}
},
"emoji-regex": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
"integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
"dev": true
},
"find-up": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
"dev": true,
"requires": {
"locate-path": "^3.0.0"
}
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
},
"locate-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
"dev": true,
"requires": {
"p-locate": "^3.0.0",
"path-exists": "^3.0.0"
}
},
"p-locate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
"dev": true,
"requires": {
"p-limit": "^2.0.0"
}
},
"path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true
},
"string-width": {
@ -1565,47 +1462,29 @@
"requires": {
"ansi-regex": "^4.1.0"
}
},
"wrap-ansi": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.0",
"string-width": "^3.0.0",
"strip-ansi": "^5.0.0"
}
},
"yargs": {
"version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
"integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
"dev": true,
"requires": {
"cliui": "^5.0.0",
"find-up": "^3.0.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^13.1.2"
}
},
"yargs-parser": {
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
"integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
"dev": true,
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
}
}
},
"yargs-parser": {
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
"integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
"dev": true,
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
},
"yargs-unparser": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz",
"integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==",
"dev": true,
"requires": {
"flat": "^4.1.0",
"lodash": "^4.17.15",
"yargs": "^13.3.0"
}
}
}
}

View File

@ -11,12 +11,13 @@
"dependencies": {
"@fortawesome/free-regular-svg-icons": "5.13.0",
"@fortawesome/free-solid-svg-icons": "5.13.0",
"lodash": "4.17.15",
"svelte-awesome": "2.3.0"
},
"devDependencies": {
"@rollup/plugin-commonjs": "11.1.0",
"@rollup/plugin-node-resolve": "7.1.3",
"mocha": "7.1.1",
"mocha": "7.1.2",
"rollup": "2.7.2",
"rollup-plugin-livereload": "1.2.0",
"rollup-plugin-svelte": "5.2.1",

View File

@ -2,7 +2,7 @@
import Icon from 'svelte-awesome'
import { faSearch } from '@fortawesome/free-solid-svg-icons'
import Node from './JSONNode.svelte'
import { search } from './utils'
import { search } from './search'
import { beforeUpdate, afterUpdate } from 'svelte'
export let searchText = ''

View File

@ -1,5 +1,6 @@
<script>
import { getType, SEARCH_PROPERTY, SEARCH_VALUE } from './utils'
import { SEARCH_PROPERTY, SEARCH_VALUE } from './search'
import { getJSONNodeType } from './utils/typeUtils.js'
export let key = 'root'
export let value
@ -12,7 +13,7 @@
let limit = DEFAULT_LIMIT
$: type = getType(value)
$: type = getJSONNodeType(value)
$: props = type === 'object'
? Object.keys(value).map(key => {

View File

@ -1,14 +1,8 @@
import { getJSONNodeType } from './utils/typeUtils'
export const SEARCH_PROPERTY = Symbol('searchProperty')
export const SEARCH_VALUE = Symbol('searchValue')
export function getType (json) {
return Array.isArray(json)
? 'array'
: json && typeof json === 'object'
? 'object'
: 'value'
}
export function search (key, value, searchText) {
let results
@ -16,7 +10,7 @@ export function search (key, value, searchText) {
results = createOrAdd(results, SEARCH_PROPERTY, true)
}
const type = getType(value)
const type = getJSONNodeType(value)
if (type === 'array') {
value.forEach((item, index) => {
let childResults = search(index, item, searchText)

58
src/types.js Normal file
View File

@ -0,0 +1,58 @@
/** JSDoc type definitions */
/**
* @typedef {{} | [] | string | number | boolean | null} JSON
*/
/**
* @typedef {{
* name: string?,
* mode: 'code' | 'form' | 'text' | 'tree' | 'view'?,
* modes: string[]?,
* history: boolean?,
* indentation: number | string?,
* onChange: function (patch: JSONPatchDocument, revert: JSONPatchDocument)?,
* onChangeText: function ()?,
* onChangeMode: function (mode: string, prevMode: string)?,
* onError: function (err: Error)?,
* isPropertyEditable: function (Path)?
* isValueEditable: function (Path)?,
* escapeUnicode: boolean?,
* expand: function(path: Path) : boolean?,
* ajv: Object?,
* ace: Object?
* }} Options
*/
/**
* @typedef {string[]} Path
*/
/**
* @typedef {{
* op: 'add' | 'remove' | 'replace' | 'copy' | 'move' | 'test',
* path: string,
* from?: string,
* value?: *
* }} JSONPatchOperation
*/
/**
* @typedef {JSONPatchOperation[]} JSONPatchDocument
*/
/**
* @typedef {{
* fromJSON: function(json: JSON, previousObject: * | undefined),
* toJSON: function(object: *),
* clone: function(object: *)
* }} JSONPatchOptions
*/
/**
* @typedef {{
* patch: JSONPatchDocument,
* revert: JSONPatchDocument,
* error: Error | null
* }} JSONPatchResult
*/

View File

@ -1,13 +0,0 @@
import { getType } from './utils.js'
import assert from 'assert'
describe('utils', () => {
it('should test equality of positive values', function () {
assert.strictEqual(getType([]), 'array')
assert.strictEqual(getType({}), 'object')
assert.strictEqual(getType(null), 'value')
assert.strictEqual(getType(2), 'value')
assert.strictEqual(getType('hello'), 'value')
assert.strictEqual(getType('hello'), 'value')
})
})

View File

@ -0,0 +1,293 @@
import { isObjectOrArray } from './typeUtils.js'
/**
* Immutability helpers
*
* inspiration:
*
* https://www.npmjs.com/package/seamless-immutable
* https://www.npmjs.com/package/ih
* https://www.npmjs.com/package/mutatis
* https://github.com/mariocasciaro/object-path-immutable
*/
/**
* Shallow clone of an Object, Array, or value
* Also copies any symbols on the Objects and Arrays
* @param {*} value
* @return {*}
*/
export function shallowCloneWithSymbols (value) {
if (Array.isArray(value)) {
// copy array items
let arr = value.slice()
// copy all symbols
Object.getOwnPropertySymbols(value).forEach(symbol => arr[symbol] = value[symbol])
return arr
}
else if (typeof value === 'object') {
// copy properties
let obj = {}
for (let prop in value) {
if (value.hasOwnProperty(prop)) {
obj[prop] = value[prop]
}
}
// copy all symbols
Object.getOwnPropertySymbols(value).forEach(symbol => obj[symbol] = value[symbol])
return obj
}
else {
return value
}
}
/**
* helper function to get a nested property in an object or array
*
* @param {Object | Array} object
* @param {Path} path
* @return {* | undefined} Returns the field when found, or undefined when the
* path doesn't exist
*/
export function getIn (object, path) {
let value = object
let i = 0
while(i < path.length) {
if (isObjectOrArray(value)) {
value = value[path[i]]
}
else {
value = undefined
}
i++
}
return value
}
/**
* helper function to replace a nested property in an object with a new value
* without mutating the object itself.
*
* @param {Object | Array} object
* @param {Path} path
* @param {*} value
* @return {Object | Array} Returns a new, updated object or array
*/
export function setIn (object, path, value) {
if (path.length === 0) {
return value
}
if (!isObjectOrArray(object)) {
throw new Error('Path does not exist')
}
const key = path[0]
const updatedValue = setIn(object[key], path.slice(1), value)
if (object[key] === updatedValue) {
// return original object unchanged when the new value is identical to the old one
return object
}
else {
const updatedObject = shallowCloneWithSymbols(object)
updatedObject[key] = updatedValue
return updatedObject
}
}
/**
* helper function to replace a nested property in an object with a new value
* without mutating the object itself.
*
* @param {Object | Array} object
* @param {Path} path
* @param {function} callback
* @return {Object | Array} Returns a new, updated object or array
*/
export function updateIn (object, path, callback) {
if (path.length === 0) {
return callback(object)
}
if (!isObjectOrArray(object)) {
throw new Error('Path doesn\'t exist')
}
const key = path[0]
const updatedValue = updateIn(object[key], path.slice(1), callback)
// TODO: create a function applyProp(...) which does the following if/else construct
if (object[key] === updatedValue) {
// return original object unchanged when the new value is identical to the old one
return object
}
else {
const updatedObject = shallowCloneWithSymbols(object)
updatedObject[key] = updatedValue
return updatedObject
}
}
/**
* helper function to delete a nested property in an object
* without mutating the object itself.
*
* @param {Object | Array} object
* @param {Path} path
* @return {Object | Array} Returns a new, updated object or array
*/
export function deleteIn (object, path) {
if (path.length === 0) {
return object
}
if (!isObjectOrArray(object)) {
return object
}
if (path.length === 1) {
const key = path[0]
if (!(key in object)) {
// key doesn't exist. return object unchanged
return object
}
else {
const updatedObject = shallowCloneWithSymbols(object)
if (Array.isArray(updatedObject) && typeof key !== 'symbol') {
updatedObject.splice(key, 1)
}
else {
delete updatedObject[key]
}
return updatedObject
}
}
const key = path[0]
const updatedValue = deleteIn(object[key], path.slice(1))
if (object[key] === updatedValue) {
// object is unchanged
return object
}
else {
const updatedObject = shallowCloneWithSymbols(object)
updatedObject[key] = updatedValue
return updatedObject
}
}
/**
* Insert a new item in an array at a specific index.
* Example usage:
*
* insertAt({arr: [1,2,3]}, ['arr', '2'], 'inserted') // [1,2,'inserted',3]
*
* @param {Object | Array} object
* @param {Path} path
* @param {*} value
* @return {Array}
*/
export function insertAt (object, path, value) {
const parentPath = path.slice(0, path.length - 1)
const index = path[path.length - 1]
return updateIn(object, parentPath, (items) => {
if (!Array.isArray(items)) {
throw new TypeError('Array expected at path ' + JSON.stringify(parentPath))
}
const updatedItems = shallowCloneWithSymbols(items)
updatedItems.splice(index, 0, value)
return updatedItems
})
}
/**
* Transform a JSON object, traverse over the whole object,
* and allow replacing Objects/Arrays/values.
* Does not iterate over symbols.
* @param {JSON} json
* @param {function (json: JSON, path: Path) : JSON} callback
* @param {Path} [path]
* @return {JSON}
*/
export function transform (json, callback, path = []) {
const updated1 = callback(json, path)
if (Array.isArray(json)) { // array
let updated2 = undefined
for (let i = 0; i < updated1.length; i++) {
const before = updated1[i]
// we stringify the index here, so the Path only contains strings and can be safely
// stringified/parsed to JSONPointer without loosing information.
// We do not want to rely on path keys being numeric/string.
const after = transform(before, callback, path.concat(i + ''))
if (after !== before) {
if (!updated2) {
updated2 = shallowCloneWithSymbols(updated1)
}
updated2[i] = after
}
}
return updated2 ? updated2 : updated1
}
else if (json && typeof json === 'object') { // object
let updated2 = undefined
for (let key in updated1) {
if (updated1.hasOwnProperty(key)) {
const before = updated1[key]
const after = transform(before, callback, path.concat(key))
if (after !== before) {
if (!updated2) {
updated2 = shallowCloneWithSymbols(updated1)
}
updated2[key] = after
}
}
}
return updated2 ? updated2 : updated1
}
else { // number, string, boolean, null
return updated1
}
}
/**
* Test whether a path exists in a JSON object
* @param {JSON} json
* @param {Path} path
* @return {boolean} Returns true if the path exists, else returns false
* @private
*/
export function existsIn (json, path) {
if (json === undefined) {
return false
}
if (path.length === 0) {
return true
}
if (Array.isArray(json)) {
// index of an array
return existsIn(json[parseInt(path[0], 10)], path.slice(1))
}
else { // Object
// object property. find the index of this property
return existsIn(json[path[0]], path.slice(1))
}
}

View File

@ -0,0 +1,323 @@
import { deleteIn, existsIn, getIn, insertAt, setIn, transform, updateIn } from './immutabilityHelpers.js'
import { expect } from './testUtils.js' // FIXME: replace jest with mocha tests, or move to jest
const test = it // FIXME: replace jest with mocha tests, or move to jest
test('getIn', () => {
const obj = {
a: {
b: {
c: 2
}
},
d: 3,
e: [
4,
{
f: 5,
g: 6
}
]
}
expect(getIn(obj, ['a', 'b'])).toEqual({c: 2})
expect(getIn(obj, ['e', '1', 'f'])).toEqual(5)
expect(getIn(obj, ['e', '999', 'f'])).toBeUndefined()
expect(getIn(obj, ['non', 'existing', 'path'])).toBeUndefined()
})
test('setIn basic', () => {
const obj = {
a: {
b: {
c: 2
}
},
d: 3
}
const updated = setIn(obj, ['a', 'b', 'c'], 4)
expect(updated).toEqual({
a: {
b: {
c: 4
}
},
d: 3
})
// original should be unchanged
expect(obj).toEqual({
a: {
b: {
c: 2
}
},
d: 3
})
expect(obj).not.toBe(updated)
})
test('setIn non existing path', () => {
const obj = {}
expect(() => setIn(obj, ['a', 'b', 'c'], 4)).toThrow(/Path does not exist/)
})
test('setIn replace value with object should throw an exception', () => {
const obj = {
a: 42,
d: 3
}
expect(() => setIn(obj, ['a', 'b', 'c'], 4)).toThrow(/Path does not exist/)
})
test('setIn replace value inside nested array', () => {
const obj = {
a: [
1,
2,
{
b: 3,
c: 4
}
],
d: 5
}
const updated = setIn(obj, ['a', '2', 'c'], 8)
expect(updated).toEqual({
a: [
1,
2,
{
b: 3,
c: 8
}
],
d: 5
})
})
test('setIn identical value should return the original object', () => {
const obj = {a:1, b:2}
const updated = setIn(obj, ['b'], 2)
expect(updated).toBe(obj) // strict equal
})
test('setIn identical value should return the original object (2)', () => {
const obj = {a:1, b: { c: 2}}
const updated = setIn(obj, ['b', 'c'], 2)
expect(updated).toBe(obj) // strict equal
})
test('updateIn', () => {
const obj = {
a: {
b: {
c: 2
}
},
d: 3
}
const updated = updateIn(obj, ['a', 'b', 'c'], (value) => value + 100)
expect(updated).toEqual({
a: {
b: {
c: 102
}
},
d: 3
})
// original should be unchanged
expect(obj).toEqual({
a: {
b: {
c: 2
}
},
d: 3
})
expect(obj).not.toBe(updated)
})
test('updateIn (2)', () => {
const obj = {
a: {
b: {
c: 2
}
},
d: 3
}
const updated = updateIn(obj, ['a', 'b' ], (obj) => [1,2,3])
expect(updated).toEqual({
a: {
b: [1,2,3]
},
d: 3
})
})
test('updateIn (3)', () => {
const obj = {
a: {
b: {
c: 2
}
},
d: 3
}
const updated = updateIn(obj, ['a', 'e' ], (value) => 'foo-' + value)
expect(updated).toEqual({
a: {
b: {
c: 2
},
e: 'foo-undefined'
},
d: 3
})
})
test('updateIn return identical value should return the original object', () => {
const obj = {
a: 2,
b: 3
}
const updated = updateIn(obj, ['b' ], (value) => 3)
expect(updated).toBe(obj)
})
test('deleteIn', () => {
const obj = {
a: {
b: {
c: 2,
d: 3
}
},
e: 4
}
const updated = deleteIn(obj, ['a', 'b', 'c'])
expect(updated).toEqual({
a: {
b: {
d: 3
}
},
e: 4
})
// original should be unchanged
expect(obj).toEqual({
a: {
b: {
c: 2,
d: 3
}
},
e: 4
})
expect(obj).not.toBe(updated)
})
test('deleteIn array', () => {
const obj = {
a: {
b: [1, {c: 2, d: 3} , 4]
},
e: 5
}
const updated = deleteIn(obj, ['a', 'b', '1', 'c'])
expect(updated).toEqual({
a: {
b: [1, {d: 3} , 4]
},
e: 5
})
// original should be unchanged
expect(obj).toEqual({
a: {
b: [1, {c: 2, d: 3} , 4]
},
e: 5
})
expect(obj).not.toBe(updated)
})
test('deleteIn non existing path', () => {
const obj = { a: {}}
const updated = deleteIn(obj, ['a', 'b'])
expect(updated).toBe(obj)
})
test('insertAt', () => {
const obj = { a: [1,2,3]}
const updated = insertAt(obj, ['a', '2'], 8)
expect(updated).toEqual({a: [1,2,8,3]})
})
test('transform (no change)', () => {
const json = {a: [1,2,3], b: {c: 4}}
const updated = transform(json, (value, path) => value)
expect(updated).toBe(json)
})
test('transform (change based on value)', () => {
const json = {a: [1,2,3], b: {c: 4}}
const updated = transform(json,
(value, path) => value === 2 ? 20 : value)
const expected = {a: [1,20,3], b: {c: 4}}
expect(updated).toEqual(expected)
expect(updated.b).toBe(json.b) // should not have replaced b
})
test('transform (change based on path)', () => {
const json = {a: [1,2,3], b: {c: 4}}
const updated = transform(json,
(value, path) => path.join('.') === 'a.1' ? 20 : value)
const expected = {a: [1,20,3], b: {c: 4}}
expect(updated).toEqual(expected)
expect(updated.b).toBe(json.b) // should not have replaced b
})
test('existsIn', () => {
const json = {
"obj": {
"arr": [1,2, {"first":3,"last":4}]
},
"str": "hello world",
"nill": null,
"bool": false
}
expect(existsIn(json, ['obj', 'arr', 2, 'first'])).toEqual(true)
expect(existsIn(json, ['obj', 'foo'])).toEqual(false)
expect(existsIn(json, ['obj', 'foo', 'bar'])).toEqual(false)
expect(existsIn(json, [])).toEqual(true)
})

View File

@ -0,0 +1,314 @@
import {
deleteIn,
existsIn,
getIn,
insertAt,
setIn
} from './immutabilityHelpers.js'
import { compileJSONPointer, parseJSONPointer } from './jsonPointer.js'
import initial from 'lodash/initial.js'
import isEqual from 'lodash/isEqual.js'
const DEFAULT_OPTIONS = {
fromJSON: (json, previousObject) => json,
toJSON: (object) => object,
clone: (object) => object
}
/**
* Apply a patch to a JSON object
* The original JSON object will not be changed,
* instead, the patch is applied in an immutable way
* @param {JSON} json
* @param {JSONPatchDocument} operations Array with JSON patch actions
* @param {JSONPatchOptions} [options]
* @return {{json: JSON, revert: JSONPatchDocument, error: Error | null}}
*/
export function immutableJSONPatch (json, operations, options = DEFAULT_OPTIONS) {
let updatedJson = json
let revert = []
for (let i = 0; i < operations.length; i++) {
const operation = operations[i]
const path = operation.path ? parseJSONPointer(operation.path) : null
const from = operation.from ? parseJSONPointer(operation.from) : null
switch (operation.op) {
case 'add': {
const result = add(updatedJson, path, operation.value, options)
updatedJson = result.json
revert = result.revert.concat(revert)
break
}
case 'remove': {
const result = remove(updatedJson, path, options)
updatedJson = result.json
revert = result.revert.concat(revert)
break
}
case 'replace': {
const result = replace(updatedJson, path, operation.value, options)
updatedJson = result.json
revert = result.revert.concat(revert)
break
}
case 'copy': {
if (!operation.from) {
return {
json: updatedJson,
revert: [],
error: new Error('Property "from" expected in copy action ' + JSON.stringify(operation))
}
}
const result = copy(updatedJson, path, from, options)
updatedJson = result.json
revert = result.revert.concat(revert)
break
}
case 'move': {
if (!operation.from) {
return {
json: updatedJson,
revert: [],
error: new Error('Property "from" expected in move action ' + JSON.stringify(operation))
}
}
const result = move(updatedJson, path, from, options)
updatedJson = result.json
revert = result.revert.concat(revert)
break
}
case 'test': {
// when a test fails, cancel the whole patch and return the error
const error = test(updatedJson, path, operation.value, options)
if (error) {
return { json, revert: [], error}
}
break
}
default: {
// unknown patch operation. Cancel the whole patch and return an error
return {
json,
revert: [],
error: new Error('Unknown JSONPatch op ' + JSON.stringify(operation.op))
}
}
}
}
return {
json: updatedJson,
revert,
error: null
}
}
/**
* Replace an existing item
* @param {JSON} json
* @param {Path} path
* @param {JSON} value
* @param {JSONPatchOptions} [options]
* @return {{json: JSON, revert: JSONPatchDocument}}
*/
export function replace (json, path, value, options) {
const oldValue = getIn(json, path)
const newValue = options.fromJSON(value, oldValue)
return {
json: setIn(json, path, newValue),
revert: [{
op: 'replace',
path: compileJSONPointer(path),
value: oldValue
}]
}
}
/**
* Remove an item or property
* @param {JSON} json
* @param {Path} path
* @param {JSONPatchOptions} [options]
* @return {{json: JSON, revert: JSONPatchDocument}}
*/
export function remove (json, path, options) {
const oldValue = getIn(json, path)
return {
json: deleteIn(json, path),
revert: [{
op: 'add',
path: compileJSONPointer(path),
value: options.toJSON(oldValue)
}]
}
}
/**
* @param {JSON} json
* @param {Path} path
* @param {JSON} value
* @param {JSONPatchOptions} [options]
* @return {{json: JSON, revert: JSONPatchDocument}}
* @private
*/
export function add (json, path, value, options) {
const resolvedPath = resolvePathIndex(json, path)
const parent = getIn(json, initial(path))
const parentIsArray = Array.isArray(parent)
const oldValue = parentIsArray
? undefined
: getIn(json, resolvedPath)
const newValue = options.fromJSON(value, oldValue)
const updatedJson = parentIsArray
? insertAt(json, resolvedPath, newValue)
: setIn(json, resolvedPath, newValue)
if (!parentIsArray && existsIn(json, resolvedPath)) {
return {
json: updatedJson,
revert: [{
op: 'replace',
path: compileJSONPointer(resolvedPath),
value: options.toJSON(oldValue)
}]
}
}
else {
return {
json: updatedJson,
revert: [{
op: 'remove',
path: compileJSONPointer(resolvedPath)
}]
}
}
}
/**
* Copy a value
* @param {JSON} json
* @param {Path} path
* @param {Path} from
* @param {JSONPatchOptions} [options]
* @return {{json: JSON, revert: JSONPatchDocument}}
* @private
*/
export function copy (json, path, from, options) {
const value = options.clone
? options.clone(getIn(json, from))
: options.fromJSON(options.toJSON(getIn(json, from)), undefined)
return add(json, path, value, {
fromJSON: DEFAULT_OPTIONS.fromJSON,
toJSON: options.toJSON,
clone: options.clone
})
}
/**
* Move a value
* @param {JSON} json
* @param {Path} path
* @param {Path} from
* @param {JSONPatchOptions} [options]
* @return {{json: JSON, revert: JSONPatchDocument}}
* @private
*/
export function move (json, path, from, options) {
const resolvedPath = resolvePathIndex(json, path)
const parent = getIn(json, initial(path))
const parentIsArray = Array.isArray(parent)
const oldValue = getIn(json, path)
const value = getIn(json, from)
const removedJson = remove(json, from, options).json
const updatedJson = parentIsArray
? insertAt(removedJson, resolvedPath, value)
: setIn(removedJson, resolvedPath, value)
if (oldValue !== undefined && !parentIsArray) {
// replaces an existing value in an object
return {
json: updatedJson,
revert: [
{
op: 'move',
from: compileJSONPointer(resolvedPath),
path: compileJSONPointer(from)
},
{
op: 'add',
path: compileJSONPointer(resolvedPath),
value: options.toJSON(oldValue)
}
]
}
}
else {
return {
json: updatedJson,
revert: [
{
op: 'move',
from: compileJSONPointer(resolvedPath),
path: compileJSONPointer(from)
}
]
}
}
}
/**
* Test whether the data contains the provided value at the specified path.
* Throws an error when the test fails.
* @param {JSON} json
* @param {Path} path
* @param {JSON} value
* @param {JSONPatchOptions} [options]
* @return {null | Error} Returns an error when the tests, returns null otherwise
*/
export function test (json, path, value, options) {
if (value === undefined) {
return new Error('Test failed, no value provided')
}
if (!existsIn(json, path)) {
return new Error('Test failed, path not found')
}
const actualValue = options.toJSON(getIn(json, path))
if (!isEqual(actualValue, value)) {
return new Error('Test failed, value differs')
}
}
/**
* Resolve the path of an index like '''
* @param {JSON} json
* @param {Path} path
* @returns {Path} Returns the resolved path
*/
export function resolvePathIndex (json, path) {
const parent = getIn(json, initial(path))
return (path[path.length - 1] === '-')
? path.slice(0, path.length - 1).concat(parent.length)
: path
}

View File

@ -0,0 +1,373 @@
import { immutableJSONPatch } from './immutableJSONPatch.js'
import { expect } from './testUtils.js' // FIXME: replace jest with mocha tests, or move to jest
const test = it // TODO: replace jest with mocha tests, or move to jest
test('test toBe', () => {
const a = { x: 2 }
const b = { x: 2 }
// just to be sure toBe does what I think it does...
expect(a).toBe(a)
expect(b).not.toBe(a)
expect(b).toEqual(a)
})
test('jsonpatch add', () => {
const json = {
arr: [1,2,3],
obj: {a : 2}
}
const patch = [
{op: 'add', path: '/obj/b', value: {foo: 'bar'}}
]
const result = immutableJSONPatch(json, patch)
expect(result.json).toEqual({
arr: [1,2,3],
obj: {a : 2, b: {foo: 'bar'}}
})
expect(result.revert).toEqual([
{op: 'remove', path: '/obj/b'}
])
expect(result.json.arr).toBe(json.arr)
})
test('jsonpatch add: insert in matrix', () => {
const json = {
arr: [1,2,3],
obj: {a : 2}
}
const patch = [
{op: 'add', path: '/arr/1', value: 4}
]
const result = immutableJSONPatch(json, patch)
expect(result.json).toEqual({
arr: [1,4,2,3],
obj: {a : 2}
})
expect(result.revert).toEqual([
{op: 'remove', path: '/arr/1'}
])
expect(result.json.obj).toBe(json.obj)
})
test('jsonpatch add: append to matrix', () => {
const json = {
arr: [1,2,3],
obj: {a : 2}
}
const patch = [
{op: 'add', path: '/arr/-', value: 4}
]
const result = immutableJSONPatch(json, patch)
expect(result.json).toEqual({
arr: [1,2,3,4],
obj: {a : 2}
})
expect(result.revert).toEqual([
{op: 'remove', path: '/arr/3'}
])
expect(result.json.obj).toBe(json.obj)
})
test('jsonpatch remove', () => {
const json = {
arr: [1,2,3],
obj: {a : 4},
unchanged: {}
}
const patch = [
{op: 'remove', path: '/obj/a'},
{op: 'remove', path: '/arr/1'},
]
const result = immutableJSONPatch(json, patch)
expect(result.json).toEqual({
arr: [1,3],
obj: {},
unchanged: {}
})
expect(result.revert).toEqual([
{op: 'add', path: '/arr/1', value: 2},
{op: 'add', path: '/obj/a', value: 4}
])
// test revert
const result2 = immutableJSONPatch(result.json, result.revert)
expect(result2.json).toEqual(json)
expect(result2.revert).toEqual(patch)
expect(result.json.unchanged).toBe(json.unchanged)
})
test('jsonpatch replace', () => {
const json = {
arr: [1,2,3],
obj: {a : 4},
unchanged: {}
}
const patch = [
{op: 'replace', path: '/obj/a', value: 400},
{op: 'replace', path: '/arr/1', value: 200},
]
const result = immutableJSONPatch(json, patch)
expect(result.json).toEqual({
arr: [1,200,3],
obj: {a: 400},
unchanged: {}
})
expect(result.revert).toEqual([
{op: 'replace', path: '/arr/1', value: 2},
{op: 'replace', path: '/obj/a', value: 4}
])
// test revert
const result2 = immutableJSONPatch(result.json, result.revert)
expect(result2.json).toEqual(json)
expect(result2.revert).toEqual([
{op: 'replace', path: '/obj/a', value: 400},
{op: 'replace', path: '/arr/1', value: 200}
])
expect(result.json.unchanged).toBe(json.unchanged)
})
test('jsonpatch copy', () => {
const json = {
arr: [1,2,3],
obj: {a : 4}
}
const patch = [
{op: 'copy', from: '/obj', path: '/arr/2'},
]
const result = immutableJSONPatch(json, patch)
expect(result.json).toEqual({
arr: [1, 2, {a:4}, 3],
obj: {a: 4}
})
expect(result.revert).toEqual([
{op: 'remove', path: '/arr/2'}
])
// test revert
const result2 = immutableJSONPatch(result.json, result.revert)
expect(result2.json).toEqual(json)
expect(result2.revert).toEqual([
{op: 'add', path: '/arr/2', value: {a: 4}}
])
expect(result.json.obj).toBe(json.obj)
expect(result.json.arr[2]).toBe(json.obj)
})
test('jsonpatch move', () => {
const json = {
arr: [1,2,3],
obj: {a : 4},
unchanged: {}
}
const patch = [
{op: 'move', from: '/obj', path: '/arr/2'},
]
const result = immutableJSONPatch(json, patch)
expect(result.error).toEqual(null)
expect(result.json).toEqual({
arr: [1, 2, {a:4}, 3],
unchanged: {}
})
expect(result.revert).toEqual([
{op: 'move', from: '/arr/2', path: '/obj'}
])
// test revert
const result2 = immutableJSONPatch(result.json, result.revert)
expect(result2.json).toEqual(json)
expect(result2.revert).toEqual(patch)
expect(result.json.arr[2]).toBe(json.obj)
expect(result.json.unchanged).toBe(json.unchanged)
})
test('jsonpatch move and replace', () => {
const json = { a: 2, b: 3 }
const patch = [
{op: 'move', from: '/a', path: '/b'},
]
const result = immutableJSONPatch(json, patch)
expect(result.json).toEqual({ b : 2 })
expect(result.revert).toEqual([
{op:'move', from: '/b', path: '/a'},
{op:'add', path:'/b', value: 3}
])
// test revert
const result2 = immutableJSONPatch(result.json, result.revert)
expect(result2.json).toEqual(json)
expect(result2.revert).toEqual([
{op: 'remove', path: '/b'},
{op: 'move', from: '/a', path: '/b'}
])
})
test('jsonpatch move and replace (nested)', () => {
const json = {
arr: [1,2,3],
obj: {a : 4},
unchanged: {}
}
const patch = [
{op: 'move', from: '/obj', path: '/arr'},
]
const result = immutableJSONPatch(json, patch)
expect(result.json).toEqual({
arr: {a:4},
unchanged: {}
})
expect(result.revert).toEqual([
{op:'move', from: '/arr', path: '/obj'},
{op:'add', path:'/arr', value: [1,2,3]}
])
// test revert
const result2 = immutableJSONPatch(result.json, result.revert)
expect(result2.json).toEqual(json)
expect(result2.revert).toEqual([
{op: 'remove', path: '/arr'},
{op: 'move', from: '/obj', path: '/arr'}
])
expect(result.json.unchanged).toBe(json.unchanged)
expect(result2.json.unchanged).toBe(json.unchanged)
})
test('jsonpatch test (ok)', () => {
const json = {
arr: [1,2,3],
obj: {a : 4}
}
const patch = [
{op: 'test', path: '/arr', value: [1,2,3]},
{op: 'add', path: '/added', value: 'ok'}
]
const result = immutableJSONPatch(json, patch)
expect(result.json).toEqual({
arr: [1,2,3],
obj: {a : 4},
added: 'ok'
})
expect(result.revert).toEqual([
{op: 'remove', path: '/added'}
])
})
test('jsonpatch test (fail: path not found)', () => {
const json = {
arr: [1,2,3],
obj: {a : 4}
}
const patch = [
{op: 'test', path: '/arr/5', value: [1,2,3]},
{op: 'add', path: '/added', value: 'ok'}
]
const result = immutableJSONPatch(json, patch)
// patch shouldn't be applied
expect(result.json).toEqual({
arr: [1,2,3],
obj: {a : 4}
})
expect(result.revert).toEqual([])
expect(result.error.toString()).toEqual('Error: Test failed, path not found')
})
test('jsonpatch test (fail: value not equal)', () => {
const json = {
arr: [1,2,3],
obj: {a : 4}
}
const patch = [
{op: 'test', path: '/obj', value: {a:4, b: 6}},
{op: 'add', path: '/added', value: 'ok'}
]
const result = immutableJSONPatch(json, patch)
// patch shouldn't be applied
expect(result.json).toEqual({
arr: [1,2,3],
obj: {a : 4}
})
expect(result.revert).toEqual([])
expect(result.error.toString()).toEqual('Error: Test failed, value differs')
})
test('jsonpatch options', () => {
const json = {
arr: [1,2,3],
obj: {a : 2}
}
const patch = [
{op: 'add', path: '/obj/a', value: 4 }
]
const result = immutableJSONPatch(json, patch, {
fromJSON: function (value, previousObject) {
return { value, previousObject }
},
toJSON: value => value
})
expect(result.json).toEqual({
arr: [1,2,3],
obj: {a : { value: 4, previousObject: 2 }}
})
const patch2 = [
{op: 'add', path: '/obj/b', value: 4 }
]
const result2 = immutableJSONPatch(json, patch2, {
fromJSON: function (value, previousObject) {
return { value, previousObject }
},
toJSON: value => value
})
expect(result2.json).toEqual({
arr: [1,2,3],
obj: {a : 2, b: { value: 4, previousObject: undefined }}
})
})
// TODO: test all operations with JSONPatchOptions (not just add)

24
src/utils/jsonPointer.js Normal file
View File

@ -0,0 +1,24 @@
/**
* Parse a JSON Pointer
* WARNING: this is not a complete implementation
* @param {string} pointer
* @return {Path}
*/
export function parseJSONPointer (pointer) {
const path = pointer.split('/')
path.shift() // remove the first empty entrypa
return path.map(p => p.replace(/~1/g, '/').replace(/~0/g, '~'))
}
/**
* Compile a JSON Pointer
* WARNING: this is not a complete implementation
* @param {Path} path
* @return {string}
*/
export function compileJSONPointer (path) {
return path
.map(p => '/' + String(p).replace(/~/g, '~0').replace(/\//g, '~1'))
.join('')
}

View File

@ -0,0 +1,20 @@
import {compileJSONPointer, parseJSONPointer} from './jsonPointer.js'
import { expect } from './testUtils.js' // FIXME: replace jest with mocha tests, or move to jest
const test = it // TODO: replace jest with mocha tests, or move to jest
test('parseJSONPointer', () => {
expect(parseJSONPointer('/obj/a')).toEqual(['obj', 'a'])
expect(parseJSONPointer('/arr/-')).toEqual(['arr', '-'])
expect(parseJSONPointer('/foo/~1~0 ~0~1')).toEqual(['foo', '/~ ~/'])
expect(parseJSONPointer('/obj')).toEqual(['obj'])
expect(parseJSONPointer('/')).toEqual([''])
expect(parseJSONPointer('')).toEqual([])
})
test('compileJSONPointer', () => {
expect(compileJSONPointer(['foo', 'bar'])).toEqual('/foo/bar')
expect(compileJSONPointer(['foo', '/~ ~/'])).toEqual('/foo/~1~0 ~0~1')
expect(compileJSONPointer([''])).toEqual('/')
expect(compileJSONPointer([])).toEqual('')
})

0
src/utils/searchUtils.js Normal file
View File

29
src/utils/testUtils.js Normal file
View File

@ -0,0 +1,29 @@
import { deepStrictEqual, strictEqual, notStrictEqual, throws } from "assert"
// TODO: integrate jest or switch to mocha
// sort of mimicking jest
export function expect (actual) {
return {
toEqual (expected) {
return deepStrictEqual(actual, expected)
},
toBe (expected) {
return strictEqual(actual, expected)
},
toBeUndefined () {
return strictEqual(actual, undefined)
},
toThrow (error) {
return throws(actual, error)
},
not: {
toBe (expected) {
return notStrictEqual(actual, expected)
}
}
}
}

123
src/utils/typeUtils.js Normal file
View File

@ -0,0 +1,123 @@
// TODO: unit test typeUtils.js
/**
* Test whether a value is an Object (and not an Array!)
*
* @param {*} value
* @return {boolean}
*/
export function isObject (value) {
return typeof value === 'object' &&
value !== null &&
!Array.isArray(value) &&
(!value._meta || typeof value._meta.value === 'undefined')
}
/**
* Test whether a value is not an object or array, but null, number, string, or
* boolean.
* @param {*} value
* @return {boolean}
*/
export function isValue (value) {
return (value === null ||
typeof value === 'number' ||
typeof value === 'string' ||
typeof value === 'boolean')
}
/**
* Test whether a value is an Object or an Array
*
* @param {*} value
* @return {boolean}
*/
export function isObjectOrArray (value) {
return typeof value === 'object' && value !== null
}
/**
* Get the JSON node type of a value: 'array', 'object', or 'value'
* @param {*} json
* @returns {string}
*/
export function getJSONNodeType (json) {
return Array.isArray(json)
? 'array'
: json && typeof json === 'object'
? 'object'
: 'value'
}
/**
* Get the type of a value
* @param {*} value
* @return {String} type
*/
export function valueType(value) {
if (value === null) {
return 'null'
}
if (value === undefined) {
return 'undefined'
}
if (typeof value === 'number') {
return 'number'
}
if (typeof value === 'string') {
return 'string'
}
if (typeof value === 'boolean') {
return 'boolean'
}
if (value instanceof RegExp) {
return 'regexp'
}
if (Array.isArray(value)) {
return 'array'
}
return 'object'
}
/**
* Test whether a text contains a url (matches when a string starts
* with 'http://*' or 'https://*' and has no whitespace characters)
* @param {String} text
*/
const isUrlRegex = /^https?:\/\/\S+$/
export function isUrl (text) {
return (typeof text === 'string') && isUrlRegex.test(text)
}
/**
* Convert contents of a string to the correct JSON type. This can be a string,
* a number, a boolean, etc
* @param {String} str
* @return {*} castedStr
* @private
*/
export function stringConvert (str) {
const num = Number(str) // will nicely fail with '123ab'
const numFloat = parseFloat(str) // will nicely fail with ' '
if (str === '') {
return ''
}
else if (str === 'null') {
return null
}
else if (str === 'true') {
return true
}
else if (str === 'false') {
return false
}
else if (!isNaN(num) && !isNaN(numFloat)) {
return num
}
else {
return str
}
}

View File

@ -0,0 +1,13 @@
import { getJSONNodeType } from './typeUtils.js'
import { expect } from './testUtils.js' // FIXME: replace jest with mocha tests, or move to jest
const test = it // TODO: replace jest with mocha tests, or move to jest
test('test getJsonType', () => {
expect(getJSONNodeType([])).toEqual('array')
expect(getJSONNodeType({})).toEqual('object')
expect(getJSONNodeType(null)).toEqual('value')
expect(getJSONNodeType(2)).toEqual('value')
expect(getJSONNodeType('hello')).toEqual('value')
expect(getJSONNodeType('hello')).toEqual('value')
})