Skip to content

Commit e9ce526

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 a6505eb commit e9ce526

15 files changed

Lines changed: 190 additions & 35 deletions

CHANGES.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,16 @@ 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
- refactor: expose JSONPathClass prototype through JSONPath for compatibility
2123
- docs: security notes
2224
- test(safe-eval): guard bind() escape route for constructor access (@brettz9)
25+
- test: restore full test coverage (@brettz9)
2326
- chore: pnpm update (@brettz9)
2427
- refactor: implement TypeScript-as-JSDoc and auto-build declaration files from this (avoiding need for maintaining declaration file manually)
2528
- 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/coverage-badge.svg

Lines changed: 1 addition & 1 deletion
Loading

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
@@ -1591,7 +1591,7 @@ function unshift(item, arr) {
15911591
* @param {unknown} val
15921592
* @param {ExpressionArray} path
15931593
* @param {ParentValue} parent
1594-
* @param {string|null} parentPropName
1594+
* @param {string|number|null} parentPropName
15951595
* @returns {boolean|null}
15961596
*/
15971597

@@ -1667,6 +1667,8 @@ function unshift(item, arr) {
16671667
* @property {JSONPathCallback} [callback]
16681668
* @property {OtherTypeCallback} [otherTypeCallback] Defaults to
16691669
* function which throws on encountering `@other`
1670+
* @property {Record<string, OtherTypeCallback>} [customTypes] Map of custom
1671+
* type operator names to their evaluation callbacks
16701672
* @property {boolean} [autostart=true]
16711673
* @property {boolean} [ignoreEvalErrors=false]
16721674
*/
@@ -1795,6 +1797,9 @@ class JSONPathClass {
17951797
/** @type {OtherTypeCallback|undefined} */
17961798
this.currOtherTypeCallback = undefined;
17971799

1800+
/** @type {Record<string, OtherTypeCallback>|undefined} */
1801+
this.currCustomTypes = undefined;
1802+
17981803
/** @type {SandboxType|undefined} */
17991804
this.currSandbox = undefined;
18001805
this._hasParentSelector = false;
@@ -1813,6 +1818,7 @@ class JSONPathClass {
18131818
this.otherTypeCallback = opts.otherTypeCallback || otherTypeCallback || function () {
18141819
throw new TypeError('You must supply an otherTypeCallback callback option ' + 'with the @other() operator.');
18151820
};
1821+
this.customTypes = opts.customTypes || {};
18161822
if (opts.autostart !== false) {
18171823
const args = /** @type {JSONPathOptions} */{
18181824
path: optObj ? opts.path : expr
@@ -1873,6 +1879,7 @@ class JSONPathClass {
18731879
this.currSandbox = this.sandbox;
18741880
callback ||= this.callback;
18751881
this.currOtherTypeCallback = otherTypeCallback || this.otherTypeCallback;
1882+
this.currCustomTypes = this.customTypes;
18761883
if (expr && typeof expr === 'object' && !Array.isArray(expr)) {
18771884
const exprObj = expr;
18781885
if (!exprObj.path && exprObj.path !== '') {
@@ -1891,6 +1898,7 @@ class JSONPathClass {
18911898
this.currEval = Object.hasOwn(exprObj, 'eval') ? exprObj.eval : this.currEval;
18921899
callback = Object.hasOwn(exprObj, 'callback') ? exprObj.callback : callback;
18931900
this.currOtherTypeCallback = Object.hasOwn(exprObj, 'otherTypeCallback') ? exprObj.otherTypeCallback : this.currOtherTypeCallback;
1901+
this.currCustomTypes = Object.hasOwn(exprObj, 'customTypes') ? exprObj.customTypes : this.currCustomTypes;
18941902
currParent = Object.hasOwn(exprObj, 'parent') ? exprObj.parent : currParent;
18951903
currParentProperty = Object.hasOwn(exprObj, 'parentProperty') ? exprObj.parentProperty : currParentProperty;
18961904
expr = exprObj.path;
@@ -2151,7 +2159,7 @@ class JSONPathClass {
21512159
} else if (loc[0] === '@') {
21522160
// value type: @boolean(), etc.
21532161
let addType = false;
2154-
const valueType = /** @type {ValueType} */loc.slice(1, -2);
2162+
const valueType = /** @type {ValueType|string} */loc.slice(1, -2);
21552163
switch (valueType) {
21562164
case 'scalar':
21572165
if (!val || !['object', 'function'].includes(typeof val)) {
@@ -2192,7 +2200,7 @@ class JSONPathClass {
21922200
}
21932201
break;
21942202
case 'other':
2195-
addType = this.currOtherTypeCallback?.(val, path, parent, /** @type {string|null} */parentPropName) ?? false;
2203+
addType = /** @type {OtherTypeCallback} */this.currOtherTypeCallback(val, path, parent, parentPropName) || false;
21962204
break;
21972205
case 'null':
21982206
if (val === null) {
@@ -2201,7 +2209,11 @@ class JSONPathClass {
22012209
break;
22022210
/* c8 ignore next 2 */
22032211
default:
2204-
throw new TypeError('Unknown value type ' + valueType);
2212+
if (this.currCustomTypes && Object.hasOwn(this.currCustomTypes, valueType)) {
2213+
addType = this.currCustomTypes[valueType](val, path, parent, parentPropName) || false;
2214+
} else {
2215+
throw new TypeError('Unknown value type ' + valueType);
2216+
}
22052217
}
22062218
if (addType) {
22072219
retObj = {
@@ -2473,7 +2485,7 @@ JSONPath.toPathArray = function (expr) {
24732485
const subx = [];
24742486
const normalized = expr
24752487
// Properties
2476-
.replaceAll(/@(?:null|boolean|number|string|integer|undefined|nonFinite|scalar|array|object|function|other)\(\)/gu, ';$&;')
2488+
.replaceAll(/@[\w$-]+\(\)/gu, ';$&;')
24772489
// Parenthetical evaluations (filtering and otherwise), directly
24782490
// within brackets or single quotes
24792491
.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
@@ -1597,7 +1597,7 @@
15971597
* @param {unknown} val
15981598
* @param {ExpressionArray} path
15991599
* @param {ParentValue} parent
1600-
* @param {string|null} parentPropName
1600+
* @param {string|number|null} parentPropName
16011601
* @returns {boolean|null}
16021602
*/
16031603

@@ -1673,6 +1673,8 @@
16731673
* @property {JSONPathCallback} [callback]
16741674
* @property {OtherTypeCallback} [otherTypeCallback] Defaults to
16751675
* function which throws on encountering `@other`
1676+
* @property {Record<string, OtherTypeCallback>} [customTypes] Map of custom
1677+
* type operator names to their evaluation callbacks
16761678
* @property {boolean} [autostart=true]
16771679
* @property {boolean} [ignoreEvalErrors=false]
16781680
*/
@@ -1801,6 +1803,9 @@
18011803
/** @type {OtherTypeCallback|undefined} */
18021804
this.currOtherTypeCallback = undefined;
18031805

1806+
/** @type {Record<string, OtherTypeCallback>|undefined} */
1807+
this.currCustomTypes = undefined;
1808+
18041809
/** @type {SandboxType|undefined} */
18051810
this.currSandbox = undefined;
18061811
this._hasParentSelector = false;
@@ -1819,6 +1824,7 @@
18191824
this.otherTypeCallback = opts.otherTypeCallback || otherTypeCallback || function () {
18201825
throw new TypeError('You must supply an otherTypeCallback callback option ' + 'with the @other() operator.');
18211826
};
1827+
this.customTypes = opts.customTypes || {};
18221828
if (opts.autostart !== false) {
18231829
const args = /** @type {JSONPathOptions} */{
18241830
path: optObj ? opts.path : expr
@@ -1879,6 +1885,7 @@
18791885
this.currSandbox = this.sandbox;
18801886
callback ||= this.callback;
18811887
this.currOtherTypeCallback = otherTypeCallback || this.otherTypeCallback;
1888+
this.currCustomTypes = this.customTypes;
18821889
if (expr && typeof expr === 'object' && !Array.isArray(expr)) {
18831890
const exprObj = expr;
18841891
if (!exprObj.path && exprObj.path !== '') {
@@ -1897,6 +1904,7 @@
18971904
this.currEval = Object.hasOwn(exprObj, 'eval') ? exprObj.eval : this.currEval;
18981905
callback = Object.hasOwn(exprObj, 'callback') ? exprObj.callback : callback;
18991906
this.currOtherTypeCallback = Object.hasOwn(exprObj, 'otherTypeCallback') ? exprObj.otherTypeCallback : this.currOtherTypeCallback;
1907+
this.currCustomTypes = Object.hasOwn(exprObj, 'customTypes') ? exprObj.customTypes : this.currCustomTypes;
19001908
currParent = Object.hasOwn(exprObj, 'parent') ? exprObj.parent : currParent;
19011909
currParentProperty = Object.hasOwn(exprObj, 'parentProperty') ? exprObj.parentProperty : currParentProperty;
19021910
expr = exprObj.path;
@@ -2157,7 +2165,7 @@
21572165
} else if (loc[0] === '@') {
21582166
// value type: @boolean(), etc.
21592167
let addType = false;
2160-
const valueType = /** @type {ValueType} */loc.slice(1, -2);
2168+
const valueType = /** @type {ValueType|string} */loc.slice(1, -2);
21612169
switch (valueType) {
21622170
case 'scalar':
21632171
if (!val || !['object', 'function'].includes(typeof val)) {
@@ -2198,7 +2206,7 @@
21982206
}
21992207
break;
22002208
case 'other':
2201-
addType = this.currOtherTypeCallback?.(val, path, parent, /** @type {string|null} */parentPropName) ?? false;
2209+
addType = /** @type {OtherTypeCallback} */this.currOtherTypeCallback(val, path, parent, parentPropName) || false;
22022210
break;
22032211
case 'null':
22042212
if (val === null) {
@@ -2207,7 +2215,11 @@
22072215
break;
22082216
/* c8 ignore next 2 */
22092217
default:
2210-
throw new TypeError('Unknown value type ' + valueType);
2218+
if (this.currCustomTypes && Object.hasOwn(this.currCustomTypes, valueType)) {
2219+
addType = this.currCustomTypes[valueType](val, path, parent, parentPropName) || false;
2220+
} else {
2221+
throw new TypeError('Unknown value type ' + valueType);
2222+
}
22112223
}
22122224
if (addType) {
22132225
retObj = {
@@ -2479,7 +2491,7 @@
24792491
const subx = [];
24802492
const normalized = expr
24812493
// Properties
2482-
.replaceAll(/@(?:null|boolean|number|string|integer|undefined|nonFinite|scalar|array|object|function|other)\(\)/gu, ';$&;')
2494+
.replaceAll(/@[\w$-]+\(\)/gu, ';$&;')
24832495
// Parenthetical evaluations (filtering and otherwise), directly
24842496
// within brackets or single quotes
24852497
.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.

0 commit comments

Comments
 (0)