Skip to content

Commit 4127a3c

Browse files
committed
Added stable JSON serialize.
Library for `fast-json-stable-stringify` would not build. Switch to `@noble/hashes`
1 parent f7cfeb1 commit 4127a3c

File tree

5 files changed

+81
-14
lines changed

5 files changed

+81
-14
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ Generates an array of numbers or prefixed strings from `start` to `end - 1`. If
218218
219219
**Example**:
220220
```jmespath
221+
range(5) // [0, 1, 2, 3, 4]
221222
range(1, 5) // [1, 2, 3, 4]
222223
range(1, 5, 'item_') // ["item_1", "item_2", "item_3", "item_4"]
223224
```
@@ -246,7 +247,7 @@ to_object([['key1', 'value1'], ['key2', 'value2']])
246247
json_serialize(value)
247248
```
248249
249-
_Uses a [deterministic version of JSON.stringify](https://www.npmjs.com/package/fast-json-stable-stringify) to serialize the value._
250+
_Uses a deterministic version of JSON.stringify to serialize the value._
250251
251252
**Description**:
252253
Serializes a JSON value to a string.

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,7 @@
8787
"vitest": "^2.1.8"
8888
},
8989
"dependencies": {
90-
"fast-json-stable-stringify": "^2.1.0",
91-
"hash.js": "^1.1.7",
90+
"@noble/hashes": "^1.7.1",
9291
"uuid": "^11.0.5"
9392
}
9493
}

src/Runtime.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ import type {
2525
} from './JSON.type';
2626
import type { TreeInterpreter } from './TreeInterpreter';
2727

28-
import stringify from 'fast-json-stable-stringify';
29-
import hash from 'hash.js';
28+
import { sha256 } from '@noble/hashes/sha256';
29+
import { sha512 } from '@noble/hashes/sha512';
30+
import { bytesToHex } from '@noble/hashes/utils';
3031
import { NIL, v4 as uuidv4, v5 as uuidv5 } from 'uuid';
32+
import jsonStringify from './utils/json-serialize';
3133

3234
export enum InputArgument {
3335
TYPE_NUMBER = 0,
@@ -631,28 +633,41 @@ export class Runtime {
631633
return condition ? thenValue : elseValue ?? null;
632634
};
633635

634-
private functionRange: RuntimeFunction<[number, number, string], Array<number | string>> = ([start, end, prefix]) => {
635-
return Array.from({ length: end - start }, (_, i) => (prefix ? `${prefix}${i + start}` : i + start));
636+
private functionRange: RuntimeFunction<[number, number?, string?], Array<number | string>> = ([
637+
start,
638+
end,
639+
prefix,
640+
]) => {
641+
if (end === undefined) {
642+
end = start;
643+
start = 0;
644+
}
645+
646+
return Array.from({ length: end - start }, (_, i) => (prefix !== undefined ? `${prefix}${i + start}` : i + start));
636647
};
637648

638649
private functionToObject: RuntimeFunction<[JSONArrayKeyValuePairs], JSONObject> = ([array]) => {
639650
return Object.fromEntries(array);
640651
};
641652

642653
private functionJsonSerialize: RuntimeFunction<[JSONValue], string> = ([inputValue]) => {
643-
return stringify(inputValue);
654+
const result = jsonStringify(inputValue);
655+
if (result === undefined) {
656+
throw new Error('invalid-value');
657+
}
658+
return result;
644659
};
645660

646661
private functionJsonParse: RuntimeFunction<[string], JSONValue> = ([inputValue]) => {
647662
return JSON.parse(inputValue);
648663
};
649664

650665
private functionSha256: RuntimeFunction<[string], string> = ([inputValue]) => {
651-
return hash.sha256().update(inputValue).digest('hex');
666+
return bytesToHex(sha256(inputValue));
652667
};
653668

654669
private functionSha512: RuntimeFunction<[string], string> = ([inputValue]) => {
655-
return hash.sha512().update(inputValue).digest('hex');
670+
return bytesToHex(sha512(inputValue));
656671
};
657672

658673
private functionUuid: RuntimeFunction<[string?, string?], string> = ([name, ns]) => {
@@ -663,7 +678,7 @@ export class Runtime {
663678
// Match the pattern between slashes and any flags after the last slash
664679
const match = regexString.match(/^\/(.*?)\/([gimsuy]*)$/);
665680
if (!match) {
666-
throw new Error('Invalid regex string format');
681+
throw new Error('invalid-regex');
667682
}
668683
return new RegExp(match[1], match[2]);
669684
}
@@ -1124,6 +1139,7 @@ export class Runtime {
11241139
},
11251140
{
11261141
types: [InputArgument.TYPE_NUMBER],
1142+
optional: true,
11271143
},
11281144
{
11291145
types: [InputArgument.TYPE_STRING],

src/utils/json-serialize.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/* eslint-disable @typescript-eslint/explicit-module-boundary-types,@typescript-eslint/no-explicit-any */
2+
3+
export default function jsonStringify(data: any): string | undefined {
4+
const seen: any[] = [];
5+
6+
function stringifyNode(node: any): string | undefined {
7+
if (node && typeof node.toJSON === 'function') {
8+
node = node.toJSON();
9+
}
10+
11+
if (node === undefined) {
12+
return;
13+
}
14+
if (typeof node === 'number') {
15+
return isFinite(node) ? String(node) : 'null';
16+
}
17+
if (typeof node !== 'object') {
18+
return JSON.stringify(node);
19+
}
20+
21+
if (Array.isArray(node)) {
22+
const arrayOutput = node.map(item => stringifyNode(item) || 'null').join(',');
23+
return `[${arrayOutput}]`;
24+
}
25+
26+
if (node === null) {
27+
return 'null';
28+
}
29+
30+
if (seen.includes(node)) {
31+
throw new TypeError('Converting circular structure to JSON');
32+
}
33+
34+
const seenIndex = seen.push(node) - 1;
35+
const keys = Object.keys(node).sort();
36+
const objectOutput = keys
37+
.map(key => {
38+
const value = stringifyNode(node[key]);
39+
return value ? `${JSON.stringify(key)}:${value}` : undefined;
40+
})
41+
.filter(entry => entry !== undefined)
42+
.join(',');
43+
44+
seen.splice(seenIndex, 1);
45+
return `{${objectOutput}}`;
46+
}
47+
48+
return stringifyNode(data);
49+
}

test/jmespath-functions.spec.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { describe, it, expect } from 'vitest';
22
import { search, registerFunction, TYPE_NUMBER } from '../src';
33
import { expectError } from './error.utils';
4-
import { sha256, sha512 } from 'hash.js';
4+
import { sha256 } from '@noble/hashes/sha256';
5+
import { sha512 } from '@noble/hashes/sha512';
6+
import { bytesToHex } from '@noble/hashes/utils';
57

68
describe('Evaluates functions', () => {
79
it('from_items()', () => {
@@ -162,11 +164,11 @@ describe('Added functions', () => {
162164
});
163165

164166
it('sha256', () => {
165-
expect(search('hello world', 'sha256(@)')).toEqual(sha256().update('hello world').digest('hex'));
167+
expect(search('hello world', 'sha256(@)')).toEqual(bytesToHex(sha256('hello world')));
166168
});
167169

168170
it('sha512', () => {
169-
expect(search('hello world', 'sha512(@)')).toEqual(sha512().update('hello world').digest('hex'));
171+
expect(search('hello world', 'sha512(@)')).toEqual(bytesToHex(sha512('hello world')));
170172
});
171173

172174
it('uuid', () => {

0 commit comments

Comments
 (0)