Skip to content

Commit fe892fc

Browse files
committed
feat: add customTypes option for providing own other type callbacks (e.g., @blob())
Also: - fix: indicate that the `OtherTypeCallback` callback type can accept a `parentPropName` with type `number` - test: restore full test coverage
1 parent cfc7ceb commit fe892fc

14 files changed

Lines changed: 189 additions & 34 deletions

‎CHANGES.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,17 @@ that usage and call JSONPath.clearCache() when cache invalidation is needed.
1313

1414
Other changes:
1515

16+
- feat: add `customTypes` option for providing own other type callbacks (e.g., `@blob()`) (@brettz9)
1617
- fix(slice): explicit zero end no longer returns the whole array (#265) (@spokodev)
17-
- fix: separate JSONPath path and script caches
18+
- fix: indicate that the `OtherTypeCallback` callback type can accept a `parentPropName` with type `number` (@brettz9)
19+
- fix: separate JSONPath path and script caches (@brettz9)
1820
- fix: restore `JSONPath.prototype.evaluate`, `safeVm`, and `vm` compatibility
1921
- fix(safe-eval): harden operator lookup against prototype inheritance (@brettz9)
2022
- fix(security): block indirect Function-constructor access in safe eval (reported by [`@c0rydoras`](https://github.com/c0rydoras): `Arthur Deierlein <info@c0rydoras.dev>`)
2123
- refactor: expose JSONPathClass prototype through JSONPath for compatibility
2224
- docs: security notes
2325
- test(safe-eval): guard bind() escape route for constructor access (@brettz9)
26+
- test: restore full test coverage (@brettz9)
2427
- chore: pnpm update (@brettz9)
2528
- refactor: implement TypeScript-as-JSDoc and auto-build declaration files from this (avoiding need for maintaining declaration file manually)
2629
- chore: update devDeps

‎README.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,11 @@ evaluate method (as the first argument) include:
211211
and it should return a boolean indicating whether the supplied value
212212
belongs to the "other" type or not (or it may handle transformations and
213213
return false).
214+
- ***customTypes*** (**default: {}**) - A key-value map of type names to functions.
215+
This allows creating custom type operators that can be used in queries
216+
(e.g., `@myType()`). The function will be invoked with the value of the item,
217+
its path, its parent, and its parent's property name. It should return a
218+
boolean indicating whether the supplied value matches the custom type.
214219

215220
### Instance methods
216221

‎badges/tests-badge.svg‎

Lines changed: 1 addition & 1 deletion
Loading

‎dist/index-browser-esm.js‎

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1595,7 +1595,7 @@ function unshift(item, arr) {
15951595
* @param {unknown} val
15961596
* @param {ExpressionArray} path
15971597
* @param {ParentValue} parent
1598-
* @param {string|null} parentPropName
1598+
* @param {string|number|null} parentPropName
15991599
* @returns {boolean|null}
16001600
*/
16011601

@@ -1671,6 +1671,8 @@ function unshift(item, arr) {
16711671
* @property {JSONPathCallback} [callback]
16721672
* @property {OtherTypeCallback} [otherTypeCallback] Defaults to
16731673
* function which throws on encountering `@other`
1674+
* @property {Record<string, OtherTypeCallback>} [customTypes] Map of custom
1675+
* type operator names to their evaluation callbacks
16741676
* @property {boolean} [autostart=true]
16751677
* @property {boolean} [ignoreEvalErrors=false]
16761678
*/
@@ -1799,6 +1801,9 @@ class JSONPathClass {
17991801
/** @type {OtherTypeCallback|undefined} */
18001802
this.currOtherTypeCallback = undefined;
18011803

1804+
/** @type {Record<string, OtherTypeCallback>|undefined} */
1805+
this.currCustomTypes = undefined;
1806+
18021807
/** @type {SandboxType|undefined} */
18031808
this.currSandbox = undefined;
18041809
this._hasParentSelector = false;
@@ -1817,6 +1822,7 @@ class JSONPathClass {
18171822
this.otherTypeCallback = opts.otherTypeCallback || otherTypeCallback || function () {
18181823
throw new TypeError('You must supply an otherTypeCallback callback option ' + 'with the @other() operator.');
18191824
};
1825+
this.customTypes = opts.customTypes || {};
18201826
if (opts.autostart !== false) {
18211827
const args = /** @type {JSONPathOptions} */{
18221828
path: optObj ? opts.path : expr
@@ -1877,6 +1883,7 @@ class JSONPathClass {
18771883
this.currSandbox = this.sandbox;
18781884
callback ||= this.callback;
18791885
this.currOtherTypeCallback = otherTypeCallback || this.otherTypeCallback;
1886+
this.currCustomTypes = this.customTypes;
18801887
if (expr && typeof expr === 'object' && !Array.isArray(expr)) {
18811888
const exprObj = expr;
18821889
if (!exprObj.path && exprObj.path !== '') {
@@ -1895,6 +1902,7 @@ class JSONPathClass {
18951902
this.currEval = Object.hasOwn(exprObj, 'eval') ? exprObj.eval : this.currEval;
18961903
callback = Object.hasOwn(exprObj, 'callback') ? exprObj.callback : callback;
18971904
this.currOtherTypeCallback = Object.hasOwn(exprObj, 'otherTypeCallback') ? exprObj.otherTypeCallback : this.currOtherTypeCallback;
1905+
this.currCustomTypes = Object.hasOwn(exprObj, 'customTypes') ? exprObj.customTypes : this.currCustomTypes;
18981906
currParent = Object.hasOwn(exprObj, 'parent') ? exprObj.parent : currParent;
18991907
currParentProperty = Object.hasOwn(exprObj, 'parentProperty') ? exprObj.parentProperty : currParentProperty;
19001908
expr = exprObj.path;
@@ -2155,7 +2163,7 @@ class JSONPathClass {
21552163
} else if (loc[0] === '@') {
21562164
// value type: @boolean(), etc.
21572165
let addType = false;
2158-
const valueType = /** @type {ValueType} */loc.slice(1, -2);
2166+
const valueType = /** @type {ValueType|string} */loc.slice(1, -2);
21592167
switch (valueType) {
21602168
case 'scalar':
21612169
if (!val || !['object', 'function'].includes(typeof val)) {
@@ -2196,7 +2204,7 @@ class JSONPathClass {
21962204
}
21972205
break;
21982206
case 'other':
2199-
addType = this.currOtherTypeCallback?.(val, path, parent, /** @type {string|null} */parentPropName) ?? false;
2207+
addType = /** @type {OtherTypeCallback} */this.currOtherTypeCallback(val, path, parent, parentPropName) || false;
22002208
break;
22012209
case 'null':
22022210
if (val === null) {
@@ -2205,7 +2213,11 @@ class JSONPathClass {
22052213
break;
22062214
/* c8 ignore next 2 */
22072215
default:
2208-
throw new TypeError('Unknown value type ' + valueType);
2216+
if (this.currCustomTypes && Object.hasOwn(this.currCustomTypes, valueType)) {
2217+
addType = this.currCustomTypes[valueType](val, path, parent, parentPropName) || false;
2218+
} else {
2219+
throw new TypeError('Unknown value type ' + valueType);
2220+
}
22092221
}
22102222
if (addType) {
22112223
retObj = {
@@ -2477,7 +2489,7 @@ JSONPath.toPathArray = function (expr) {
24772489
const subx = [];
24782490
const normalized = expr
24792491
// Properties
2480-
.replaceAll(/@(?:null|boolean|number|string|integer|undefined|nonFinite|scalar|array|object|function|other)\(\)/gu, ';$&;')
2492+
.replaceAll(/@[\w$-]+\(\)/gu, ';$&;')
24812493
// Parenthetical evaluations (filtering and otherwise), directly
24822494
// within brackets or single quotes
24832495
.replaceAll(/[['](\??\(.*?\))[\]'](?!.\])/gu, function ($0, $1) {

‎dist/index-browser-esm.min.js‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎dist/index-browser-esm.min.js.map‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎dist/index-browser-umd.cjs‎

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1601,7 +1601,7 @@
16011601
* @param {unknown} val
16021602
* @param {ExpressionArray} path
16031603
* @param {ParentValue} parent
1604-
* @param {string|null} parentPropName
1604+
* @param {string|number|null} parentPropName
16051605
* @returns {boolean|null}
16061606
*/
16071607

@@ -1677,6 +1677,8 @@
16771677
* @property {JSONPathCallback} [callback]
16781678
* @property {OtherTypeCallback} [otherTypeCallback] Defaults to
16791679
* function which throws on encountering `@other`
1680+
* @property {Record<string, OtherTypeCallback>} [customTypes] Map of custom
1681+
* type operator names to their evaluation callbacks
16801682
* @property {boolean} [autostart=true]
16811683
* @property {boolean} [ignoreEvalErrors=false]
16821684
*/
@@ -1805,6 +1807,9 @@
18051807
/** @type {OtherTypeCallback|undefined} */
18061808
this.currOtherTypeCallback = undefined;
18071809

1810+
/** @type {Record<string, OtherTypeCallback>|undefined} */
1811+
this.currCustomTypes = undefined;
1812+
18081813
/** @type {SandboxType|undefined} */
18091814
this.currSandbox = undefined;
18101815
this._hasParentSelector = false;
@@ -1823,6 +1828,7 @@
18231828
this.otherTypeCallback = opts.otherTypeCallback || otherTypeCallback || function () {
18241829
throw new TypeError('You must supply an otherTypeCallback callback option ' + 'with the @other() operator.');
18251830
};
1831+
this.customTypes = opts.customTypes || {};
18261832
if (opts.autostart !== false) {
18271833
const args = /** @type {JSONPathOptions} */{
18281834
path: optObj ? opts.path : expr
@@ -1883,6 +1889,7 @@
18831889
this.currSandbox = this.sandbox;
18841890
callback ||= this.callback;
18851891
this.currOtherTypeCallback = otherTypeCallback || this.otherTypeCallback;
1892+
this.currCustomTypes = this.customTypes;
18861893
if (expr && typeof expr === 'object' && !Array.isArray(expr)) {
18871894
const exprObj = expr;
18881895
if (!exprObj.path && exprObj.path !== '') {
@@ -1901,6 +1908,7 @@
19011908
this.currEval = Object.hasOwn(exprObj, 'eval') ? exprObj.eval : this.currEval;
19021909
callback = Object.hasOwn(exprObj, 'callback') ? exprObj.callback : callback;
19031910
this.currOtherTypeCallback = Object.hasOwn(exprObj, 'otherTypeCallback') ? exprObj.otherTypeCallback : this.currOtherTypeCallback;
1911+
this.currCustomTypes = Object.hasOwn(exprObj, 'customTypes') ? exprObj.customTypes : this.currCustomTypes;
19041912
currParent = Object.hasOwn(exprObj, 'parent') ? exprObj.parent : currParent;
19051913
currParentProperty = Object.hasOwn(exprObj, 'parentProperty') ? exprObj.parentProperty : currParentProperty;
19061914
expr = exprObj.path;
@@ -2161,7 +2169,7 @@
21612169
} else if (loc[0] === '@') {
21622170
// value type: @boolean(), etc.
21632171
let addType = false;
2164-
const valueType = /** @type {ValueType} */loc.slice(1, -2);
2172+
const valueType = /** @type {ValueType|string} */loc.slice(1, -2);
21652173
switch (valueType) {
21662174
case 'scalar':
21672175
if (!val || !['object', 'function'].includes(typeof val)) {
@@ -2202,7 +2210,7 @@
22022210
}
22032211
break;
22042212
case 'other':
2205-
addType = this.currOtherTypeCallback?.(val, path, parent, /** @type {string|null} */parentPropName) ?? false;
2213+
addType = /** @type {OtherTypeCallback} */this.currOtherTypeCallback(val, path, parent, parentPropName) || false;
22062214
break;
22072215
case 'null':
22082216
if (val === null) {
@@ -2211,7 +2219,11 @@
22112219
break;
22122220
/* c8 ignore next 2 */
22132221
default:
2214-
throw new TypeError('Unknown value type ' + valueType);
2222+
if (this.currCustomTypes && Object.hasOwn(this.currCustomTypes, valueType)) {
2223+
addType = this.currCustomTypes[valueType](val, path, parent, parentPropName) || false;
2224+
} else {
2225+
throw new TypeError('Unknown value type ' + valueType);
2226+
}
22152227
}
22162228
if (addType) {
22172229
retObj = {
@@ -2483,7 +2495,7 @@
24832495
const subx = [];
24842496
const normalized = expr
24852497
// Properties
2486-
.replaceAll(/@(?:null|boolean|number|string|integer|undefined|nonFinite|scalar|array|object|function|other)\(\)/gu, ';$&;')
2498+
.replaceAll(/@[\w$-]+\(\)/gu, ';$&;')
24872499
// Parenthetical evaluations (filtering and otherwise), directly
24882500
// within brackets or single quotes
24892501
.replaceAll(/[['](\??\(.*?\))[\]'](?!.\])/gu, function ($0, $1) {

‎dist/index-browser-umd.min.cjs‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎dist/index-browser-umd.min.cjs.map‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎dist/index-node-cjs.cjs‎

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1599,7 +1599,7 @@ function unshift(item, arr) {
15991599
* @param {unknown} val
16001600
* @param {ExpressionArray} path
16011601
* @param {ParentValue} parent
1602-
* @param {string|null} parentPropName
1602+
* @param {string|number|null} parentPropName
16031603
* @returns {boolean|null}
16041604
*/
16051605

@@ -1675,6 +1675,8 @@ function unshift(item, arr) {
16751675
* @property {JSONPathCallback} [callback]
16761676
* @property {OtherTypeCallback} [otherTypeCallback] Defaults to
16771677
* function which throws on encountering `@other`
1678+
* @property {Record<string, OtherTypeCallback>} [customTypes] Map of custom
1679+
* type operator names to their evaluation callbacks
16781680
* @property {boolean} [autostart=true]
16791681
* @property {boolean} [ignoreEvalErrors=false]
16801682
*/
@@ -1803,6 +1805,9 @@ class JSONPathClass {
18031805
/** @type {OtherTypeCallback|undefined} */
18041806
this.currOtherTypeCallback = undefined;
18051807

1808+
/** @type {Record<string, OtherTypeCallback>|undefined} */
1809+
this.currCustomTypes = undefined;
1810+
18061811
/** @type {SandboxType|undefined} */
18071812
this.currSandbox = undefined;
18081813
this._hasParentSelector = false;
@@ -1821,6 +1826,7 @@ class JSONPathClass {
18211826
this.otherTypeCallback = opts.otherTypeCallback || otherTypeCallback || function () {
18221827
throw new TypeError('You must supply an otherTypeCallback callback option ' + 'with the @other() operator.');
18231828
};
1829+
this.customTypes = opts.customTypes || {};
18241830
if (opts.autostart !== false) {
18251831
const args = /** @type {JSONPathOptions} */{
18261832
path: optObj ? opts.path : expr
@@ -1881,6 +1887,7 @@ class JSONPathClass {
18811887
this.currSandbox = this.sandbox;
18821888
callback ||= this.callback;
18831889
this.currOtherTypeCallback = otherTypeCallback || this.otherTypeCallback;
1890+
this.currCustomTypes = this.customTypes;
18841891
if (expr && typeof expr === 'object' && !Array.isArray(expr)) {
18851892
const exprObj = expr;
18861893
if (!exprObj.path && exprObj.path !== '') {
@@ -1899,6 +1906,7 @@ class JSONPathClass {
18991906
this.currEval = Object.hasOwn(exprObj, 'eval') ? exprObj.eval : this.currEval;
19001907
callback = Object.hasOwn(exprObj, 'callback') ? exprObj.callback : callback;
19011908
this.currOtherTypeCallback = Object.hasOwn(exprObj, 'otherTypeCallback') ? exprObj.otherTypeCallback : this.currOtherTypeCallback;
1909+
this.currCustomTypes = Object.hasOwn(exprObj, 'customTypes') ? exprObj.customTypes : this.currCustomTypes;
19021910
currParent = Object.hasOwn(exprObj, 'parent') ? exprObj.parent : currParent;
19031911
currParentProperty = Object.hasOwn(exprObj, 'parentProperty') ? exprObj.parentProperty : currParentProperty;
19041912
expr = exprObj.path;
@@ -2159,7 +2167,7 @@ class JSONPathClass {
21592167
} else if (loc[0] === '@') {
21602168
// value type: @boolean(), etc.
21612169
let addType = false;
2162-
const valueType = /** @type {ValueType} */loc.slice(1, -2);
2170+
const valueType = /** @type {ValueType|string} */loc.slice(1, -2);
21632171
switch (valueType) {
21642172
case 'scalar':
21652173
if (!val || !['object', 'function'].includes(typeof val)) {
@@ -2200,7 +2208,7 @@ class JSONPathClass {
22002208
}
22012209
break;
22022210
case 'other':
2203-
addType = this.currOtherTypeCallback?.(val, path, parent, /** @type {string|null} */parentPropName) ?? false;
2211+
addType = /** @type {OtherTypeCallback} */this.currOtherTypeCallback(val, path, parent, parentPropName) || false;
22042212
break;
22052213
case 'null':
22062214
if (val === null) {
@@ -2209,7 +2217,11 @@ class JSONPathClass {
22092217
break;
22102218
/* c8 ignore next 2 */
22112219
default:
2212-
throw new TypeError('Unknown value type ' + valueType);
2220+
if (this.currCustomTypes && Object.hasOwn(this.currCustomTypes, valueType)) {
2221+
addType = this.currCustomTypes[valueType](val, path, parent, parentPropName) || false;
2222+
} else {
2223+
throw new TypeError('Unknown value type ' + valueType);
2224+
}
22132225
}
22142226
if (addType) {
22152227
retObj = {
@@ -2481,7 +2493,7 @@ JSONPath.toPathArray = function (expr) {
24812493
const subx = [];
24822494
const normalized = expr
24832495
// Properties
2484-
.replaceAll(/@(?:null|boolean|number|string|integer|undefined|nonFinite|scalar|array|object|function|other)\(\)/gu, ';$&;')
2496+
.replaceAll(/@[\w$-]+\(\)/gu, ';$&;')
24852497
// Parenthetical evaluations (filtering and otherwise), directly
24862498
// within brackets or single quotes
24872499
.replaceAll(/[['](\??\(.*?\))[\]'](?!.\])/gu, function ($0, $1) {

0 commit comments

Comments
 (0)