From 7590dc09a496012e0cf018d4d53772404ea369ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Mon, 29 Jan 2024 01:49:28 +0100 Subject: [PATCH] feat: introduce static query method --- README.md | 337 ++++++++++++----------------------- package-lock.json | 4 +- package.json | 4 +- src/__tests__/index.test.mjs | 5 +- src/core/index.mjs | 6 + src/index.d.ts | 19 +- 6 files changed, 142 insertions(+), 233 deletions(-) diff --git a/README.md b/README.md index 5bc68fc..8bc8488 100644 --- a/README.md +++ b/README.md @@ -33,241 +33,140 @@ ## Usage -```js -import Nimma from 'https://cdn.skypack.dev/nimma'; - -const n = new Nimma([ - '$.info', - '$.info.contact', - '$.info^', - '$.info^~', - '$.servers[*].url', - '$.servers[0:2]', - '$.servers[:5]', - "$.bar['children']", - "$.bar['0']", - "$.bar['children.bar']", - '$.channels[*][publish,subscribe][?(@.schemaFormat === void 0)].payload', - "$..[?( @property === 'get' || @property === 'put' || @property === 'post' )]", - "$..paths..[?( @property === 'get' || @property === 'put' || @property === 'post' )]", - `$.examples.*`, - '$[1:-5:-2]', - '$..foo..[?( @property >= 900 )]..foo', -]); - -// you can perform the query... -n.query(document, { - ['$.info']({ value, path }) { - // - }, - // and so on for each specified path -}); - -// ... or write the generated code. It's advisable to write the code to further re-use. -await cache.writeFile('./nimma-code.mjs', n.sourceCode); // once -``` - -Here's how the sourceCode would look like for the above path expressions +### Querying ```js -import { Scope, isObject, inBounds } from 'nimma/runtime'; -const tree = { - '$.info': function (scope, fn) { - const value = scope.sandbox.root; - if (isObject(value)) { - scope.fork(['info'])?.emit(fn, 0, false); +import Nimma from 'nimma'; + +const document = { + info: { + title: 'Example API', + version: '1.0.0', + contact: { + name: 'API Support', + url: 'http://www.example.com/support', + email: '' } }, - '$.info.contact': function (scope, fn) { - const value = scope.sandbox.root?.['info']; - if (isObject(value)) { - scope.fork(['info', 'contact'])?.emit(fn, 0, false); - } - }, - '$.info^': function (scope, fn) { - const value = scope.sandbox.root; - if (isObject(value)) { - scope.fork(['info'])?.emit(fn, 1, false); - } - }, - '$.info^~': function (scope, fn) { - const value = scope.sandbox.root; - if (isObject(value)) { - scope.fork(['info'])?.emit(fn, 1, true); - } - }, - '$.servers[*].url': function (scope, fn) { - if (scope.depth !== 2) return; - if (scope.path[0] !== 'servers') return; - if (scope.path[2] !== 'url') return; - scope.emit(fn, 0, false); - }, - '$.servers[0:2]': function (scope, fn) { - if (scope.depth !== 1) return; - if (scope.path[0] !== 'servers') return; - if (typeof scope.path[1] !== 'number' || scope.path[1] >= 2) return; - scope.emit(fn, 0, false); - }, - '$.servers[:5]': function (scope, fn) { - if (scope.depth !== 1) return; - if (scope.path[0] !== 'servers') return; - if (typeof scope.path[1] !== 'number' || scope.path[1] >= 5) return; - scope.emit(fn, 0, false); - }, - "$.bar['children']": function (scope, fn) { - const value = scope.sandbox.root?.['bar']; - if (isObject(value)) { - scope.fork(['bar', 'children'])?.emit(fn, 0, false); + paths: { + '/users': { + get: { + summary: 'Returns a list of users.', + operationId: 'getUsers', + responses: { + '200': { + description: 'OK', + } + } + }, + post: { + summary: 'Creates a new user.', + operationId: 'createUser', + responses: { + '200': { + description: 'OK', + } + } + }, + put: { + summary: 'Updates a user.', + operationId: 'updateUser', + responses: { + '200': { + description: 'OK', + } + } + } } + } +}; + +const query = Nimma.query(document, { + '$.info'({ path, value }) { + console.log(path, value); }, - "$.bar['0']": function (scope, fn) { - const value = scope.sandbox.root?.['bar']; - if (isObject(value)) { - scope.fork(['bar', '0'])?.emit(fn, 0, false); - } + '$.info.contact'({ path, value }) { + console.log(path, value); }, - "$.bar['children.bar']": function (scope, fn) { - const value = scope.sandbox.root?.['bar']; - if (isObject(value)) { - scope.fork(['bar', 'children.bar'])?.emit(fn, 0, false); + '$.paths[*][get,post]'({ path, value }) { + console.log(path, value); + } +}); + +// a given instance can be re-used to traverse another document +query({ + info: { + title: 'Example API', + version: '2.0.0', + contact: { + email: '' } }, - '$.channels[*][publish,subscribe][?(@.schemaFormat === void 0)].payload': - function (scope, fn) { - if (scope.depth !== 4) return; - if (scope.path[0] !== 'channels') return; - if (scope.path[2] !== 'publish' && scope.path[2] !== 'subscribe') return; - if (!(scope.sandbox.at(3).value.schemaFormat === void 0)) return; - if (scope.path[4] !== 'payload') return; - scope.emit(fn, 0, false); - }, - "$..[?( @property === 'get' || @property === 'put' || @property === 'post' )]": - function (scope, fn) { - if ( - !( - scope.sandbox.property === 'get' || - scope.sandbox.property === 'put' || - scope.sandbox.property === 'post' - ) - ) - return; - scope.emit(fn, 0, false); - }, - "$..paths..[?( @property === 'get' || @property === 'put' || @property === 'post' )]": - function (scope, fn) { - if (scope.depth < 1) return; - let pos = 0; - if (((pos = scope.path.indexOf('paths', pos)), pos === -1)) return; - if ( - scope.depth < pos + 1 || - ((pos = !( - scope.sandbox.property === 'get' || - scope.sandbox.property === 'put' || - scope.sandbox.property === 'post' - ) - ? -1 - : scope.depth), - pos === -1) - ) - return; - if (scope.depth !== pos) return; - scope.emit(fn, 0, false); - }, - '$.examples.*': function (scope, fn) { - if (scope.depth !== 1) return; - if (scope.path[0] !== 'examples') return; - scope.emit(fn, 0, false); - }, - '$[1:-5:-2]': function (scope, fn) { - if (scope.depth !== 0) return; - if ( - typeof scope.path[0] !== 'number' || - !inBounds(scope.sandbox.parentValue, scope.path[0], 1, -5, -2) - ) - return; - scope.emit(fn, 0, false); +}); +``` + +### Code Generation + +Nimma can also generate a JS code that can be used to traverse a given JSON document. + +```js +import Nimma from 'nimma'; +import * as fs from 'node:fs/promises'; + +const nimma = new Nimma([ + '$.info', + '$.info.contact', + '$.servers[:5]', + '$.paths[*][*]' +], { + module: 'esm' // or 'cjs' for CommonJS. 'esm' is the default value +}); + +// for esm +await fs.writeFile('./nimma-code.mjs', nimma.sourceCode); + +// for cjs +await fs.writeFile('./nimma-code.cjs', nimma.sourceCode); + +// you can also use the code directly +nimma.query(document, { + // you need to provide a callback for each JSON Path expression + '$.info'({ path, value }) { + console.log(path, value); }, - '$..foo..[?( @property >= 900 )]..foo': function (scope, fn) { - scope.bail( - '$..foo..[?( @property >= 900 )]..foo', - scope => scope.emit(fn, 0, false), - [ - { - fn: scope => scope.property !== 'foo', - deep: true, - }, - { - fn: scope => !(scope.sandbox.property >= 900), - deep: true, - }, - { - fn: scope => scope.property !== 'foo', - deep: true, - }, - ], - ); + '$.info.contact'({ path, value }) { + console.log(path, value); + }, + '$.servers[:5]'({ path, value }) { + console.log(path, value); }, -}; -export default function (input, callbacks) { - const scope = new Scope(input); - const _tree = scope.registerTree(tree); - const _callbacks = scope.proxyCallbacks(callbacks, {}); - try { - _tree['$.info'](scope, _callbacks['$.info']); - _tree['$.info.contact'](scope, _callbacks['$.info.contact']); - _tree['$.info^'](scope, _callbacks['$.info^']); - _tree['$.info^~'](scope, _callbacks['$.info^~']); - _tree["$.bar['children']"](scope, _callbacks["$.bar['children']"]); - _tree["$.bar['0']"](scope, _callbacks["$.bar['0']"]); - _tree["$.bar['children.bar']"](scope, _callbacks["$.bar['children.bar']"]); - _tree['$..foo..[?( @property >= 900 )]..foo']( - scope, - _callbacks['$..foo..[?( @property >= 900 )]..foo'], - ); - scope.traverse(() => { - _tree['$.servers[*].url'](scope, _callbacks['$.servers[*].url']); - _tree['$.servers[0:2]'](scope, _callbacks['$.servers[0:2]']); - _tree['$.servers[:5]'](scope, _callbacks['$.servers[:5]']); - _tree[ - '$.channels[*][publish,subscribe][?(@.schemaFormat === void 0)].payload' - ]( - scope, - _callbacks[ - '$.channels[*][publish,subscribe][?(@.schemaFormat === void 0)].payload' - ], - ); - _tree[ - "$..[?( @property === 'get' || @property === 'put' || @property === 'post' )]" - ]( - scope, - _callbacks[ - "$..[?( @property === 'get' || @property === 'put' || @property === 'post' )]" - ], - ); - _tree[ - "$..paths..[?( @property === 'get' || @property === 'put' || @property === 'post' )]" - ]( - scope, - _callbacks[ - "$..paths..[?( @property === 'get' || @property === 'put' || @property === 'post' )]" - ], - ); - _tree['$.examples.*'](scope, _callbacks['$.examples.*']); - _tree['$[1:-5:-2]'](scope, _callbacks['$[1:-5:-2]']); - }); - } finally { - scope.destroy(); + '$.paths[*][*]'({ path, value }) { + console.log(path, value); } -} +}); ``` -Since it's a valid ES Module, you can easily load it again and there's no need for `new Nimma`. +Once the code is written to the file, you can use it as follows: -### Supported opts +```js +import query from './nimma-code.mjs'; // or const query = require('./nimma-code.cjs'); -- output: ES2018 | ES2021 | auto -- fallback -- unsafe +query(document, { + // you need to provide a callback for each JSON Path expression + '$.info'({ path, value }) { + console.log(path, value); + }, + '$.info.contact'({ path, value }) { + console.log(path, value); + }, + '$.servers[:5]'({ path, value }) { + console.log(path, value); + }, + '$.paths[*][*]'({ path, value }) { + console.log(path, value); + } +}); +``` ## Comparison vs jsonpath-plus and alikes diff --git a/package-lock.json b/package-lock.json index 33e9e2b..7316b8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nimma", - "version": "0.3.1", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nimma", - "version": "0.3.1", + "version": "0.4.0", "license": "Apache-2.0", "dependencies": { "@jsep-plugin/regex": "^1.0.3", diff --git a/package.json b/package.json index ee080c9..804b886 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nimma", - "version": "0.3.1", + "version": "0.4.0", "description": "Scalable JSONPath engine.", "keywords": [ "json", @@ -57,7 +57,7 @@ "build": "export NODE_ENV=production; rollup -c", "lint": "ls-lint && eslint --cache --cache-location .cache/ src && prettier --ignore-path .gitignore --check --cache --cache-location .cache/.prettier src", "test": "export NODE_ENV=test; c8 mocha --config .mocharc ./**/__tests__/**/*.test.mjs && karma start karma.conf.cjs", - "prepublishOnly": "npm run lint && npm run test && npm run build && (stat ./dist/esm/parser/parser.mjs && stat ./dist/cjs/parser/parser.js) >> /dev/null" + "prepublishOnly": "npm run lint && npm run test && npm run build" }, "devDependencies": { "@babel/core": "^7.23.9", diff --git a/src/__tests__/index.test.mjs b/src/__tests__/index.test.mjs index c798d0d..1440592 100644 --- a/src/__tests__/index.test.mjs +++ b/src/__tests__/index.test.mjs @@ -12,14 +12,13 @@ function collect(input, expressions, opts) { collected[expr].push([scope.value, scope.path]); }; - const n = new Nimma(expressions, opts); - - n.query( + Nimma.query( input, expressions.reduce((mapped, expression) => { mapped[expression] = _.bind(null, expression); return mapped; }, {}), + opts, ); return collected; diff --git a/src/core/index.mjs b/src/core/index.mjs index 9a922c4..177af8a 100644 --- a/src/core/index.mjs +++ b/src/core/index.mjs @@ -31,4 +31,10 @@ export default class Nimma { this.#compiledFn(input, callbacks); } + + static query(input, callbacks, options) { + const nimma = new Nimma(Object.keys(callbacks), options); + nimma.query(input, callbacks); + return input => nimma.query(input, callbacks); + } } diff --git a/src/index.d.ts b/src/index.d.ts index 80ac53e..f9baee3 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -7,18 +7,23 @@ export type EmittedScope = { readonly value: unknown; }; +export type Options = { + customShorthands?: Record | null; + module?: 'esm' | 'commonjs'; +}; + declare class Nimma { public readonly sourceCode: string; - constructor( - expressions: string[], - opts?: { - customShorthands?: Record | null; - module?: 'esm' | 'commonjs'; - }, - ); + constructor(expressions: string[], opts?: Options); public query(input: unknown, callbacks: Record): void; + + static query( + input: unknown, + callbacks: Record, + opts?: Options, + ): (input: unknown) => void; } export default Nimma;