Skip to content

Commit

Permalink
feat: introduce static query method
Browse files Browse the repository at this point in the history
  • Loading branch information
P0lip committed Jan 29, 2024
1 parent 42f7d3e commit 7590dc0
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 233 deletions.
337 changes: 118 additions & 219 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nimma",
"version": "0.3.1",
"version": "0.4.0",
"description": "Scalable JSONPath engine.",
"keywords": [
"json",
Expand Down Expand Up @@ -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",
Expand Down
Loading

0 comments on commit 7590dc0

Please sign in to comment.