Skip to content

Commit 4b791da

Browse files
committed
feat: 🎸 add initial implementation
1 parent debf8d4 commit 4b791da

32 files changed

+1166
-3
lines changed

‎package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,12 @@
4040
"format:write": "biome format --write ./src",
4141
"lint": "biome lint ./src",
4242
"lint:fix": "biome lint --apply ./src",
43-
"clean": "npx rimraf lib typedocs coverage gh-pages yarn-error.log",
43+
"clean": "npx rimraf@6.0.1 lib typedocs coverage gh-pages yarn-error.log",
4444
"build": "tsc --project tsconfig.build.json --module commonjs --target es2020 --outDir lib",
45-
"test": "vitest /src",
45+
"test": "vitest ./src",
4646
"coverage": "yarn test --collectCoverage",
4747
"typedoc": "npx typedoc@0.25.13 --tsconfig tsconfig.build.json",
48-
"build:pages": "npx rimraf@5.0.5 gh-pages && mkdir -p gh-pages && cp -r typedocs/* gh-pages && cp -r coverage gh-pages/coverage",
48+
"build:pages": "npx rimraf@6.0.1 gh-pages && mkdir -p gh-pages && cp -r typedocs/* gh-pages && cp -r coverage gh-pages/coverage",
4949
"deploy:pages": "gh-pages -d gh-pages",
5050
"publish-coverage-and-typedocs": "yarn typedoc && yarn coverage && yarn build:pages && yarn deploy:pages"
5151
},
@@ -56,8 +56,10 @@
5656
"@jsonjoy.com/util": "^1.3.0"
5757
},
5858
"devDependencies": {
59+
"@biomejs/biome": "^1.9.3",
5960
"@types/benchmark": "^2.1.5",
6061
"benchmark": "^2.1.4",
62+
"config-galore": "^1.0.0",
6163
"tslib": "^2.7.0",
6264
"typescript": "^5.6.2",
6365
"vitest": "^2.1.2"

‎src/__bench__/find.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const Benchmark = require('benchmark');
2+
const {parseJsonPointer} = require('../../../es6/json-pointer');
3+
const {find} = require('../../../es6/json-pointer/find');
4+
const {parseJsonPointer: parseJsonPointerEs5} = require('../../../lib/json-pointer');
5+
const {find: findEs5} = require('../../../lib/json-pointer/find');
6+
const {findByPointer: findByPointerV1} = require('../../../es6/json-pointer/findByPointer/v1');
7+
const {findByPointer: findByPointerV2} = require('../../../es6/json-pointer/findByPointer/v2');
8+
const {findByPointer: findByPointerV3} = require('../../../es6/json-pointer/findByPointer/v3');
9+
const {findByPointer: findByPointerV4} = require('../../../es6/json-pointer/findByPointer/v4');
10+
const {findByPointer: findByPointerV5} = require('../../../es6/json-pointer/findByPointer/v5');
11+
const {findByPointer: findByPointerV6} = require('../../../es6/json-pointer/findByPointer/v6');
12+
13+
const suite = new Benchmark.Suite();
14+
15+
const doc = {
16+
foo: {
17+
bar: [
18+
{
19+
baz: 123,
20+
},
21+
],
22+
},
23+
};
24+
25+
suite
26+
.add(`find`, function () {
27+
const pointer = parseJsonPointer('/foo/bar/0/baz');
28+
find(doc, pointer);
29+
})
30+
.add(`find ES5`, function () {
31+
const pointer = parseJsonPointerEs5('/foo/bar/0/baz');
32+
findEs5(doc, pointer);
33+
})
34+
.add(`findByPointer (v1)`, function () {
35+
findByPointerV1('/foo/bar/0/baz', doc);
36+
})
37+
.add(`findByPointer (v2)`, function () {
38+
findByPointerV2('/foo/bar/0/baz', doc);
39+
})
40+
.add(`findByPointer (v3)`, function () {
41+
findByPointerV3('/foo/bar/0/baz', doc);
42+
})
43+
.add(`findByPointer (v4)`, function () {
44+
findByPointerV4('/foo/bar/0/baz', doc);
45+
})
46+
.add(`findByPointer (v5)`, function () {
47+
findByPointerV5('/foo/bar/0/baz', doc);
48+
})
49+
.add(`findByPointer (v6)`, function () {
50+
findByPointerV6('/foo/bar/0/baz', doc);
51+
})
52+
.on('cycle', function (event) {
53+
console.log(String(event.target));
54+
})
55+
.on('complete', function () {
56+
console.log('Fastest is ' + this.filter('fastest').map('name'));
57+
})
58+
.run();

‎src/__bench__/parseJsonPointer.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/* tslint:disable no-console */
2+
3+
import * as Benchmark from 'benchmark';
4+
import {parseJsonPointer} from '../util';
5+
6+
const suite = new Benchmark.Suite();
7+
8+
suite
9+
.add(`parseJsonPointer ""`, () => {
10+
parseJsonPointer('');
11+
})
12+
.add(`parseJsonPointer "/"`, () => {
13+
parseJsonPointer('/');
14+
})
15+
.add(`parseJsonPointer "/foo"`, () => {
16+
parseJsonPointer('/foo');
17+
})
18+
.add(`parseJsonPointer "/foo/bar/baz"`, () => {
19+
parseJsonPointer('/foo/bar/baz');
20+
})
21+
.add(`parseJsonPointer "/foo/bar/baz/layer~0/123/ok~1test/4"`, () => {
22+
parseJsonPointer('/foo/bar/baz/layer~0/123/ok~1test/4');
23+
})
24+
.on('cycle', (event: any) => {
25+
console.log(String(event.target));
26+
})
27+
.on('complete', () => {
28+
console.log('Fastest is ' + suite.filter('fastest').map('name'));
29+
})
30+
.run();

‎src/__demos__/json-pointer.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/* tslint:disable no-console */
2+
3+
/**
4+
* Run this demo with:
5+
*
6+
* npx ts-node src/json-pointer/__demos__/json-pointer.ts
7+
*/
8+
9+
import {find, unescapeComponent, escapeComponent, parseJsonPointer, formatJsonPointer} from '../../json-pointer';
10+
11+
const doc = {
12+
foo: {
13+
bar: 123,
14+
},
15+
};
16+
17+
const path = parseJsonPointer('/foo/bar');
18+
const ref = find(doc, path);
19+
20+
console.log(ref);
21+
// { val: 123, obj: { bar: 123 }, key: 'bar' }
22+
23+
console.log(parseJsonPointer('/f~0o~1o/bar/1/baz'));
24+
// [ 'f~o/o', 'bar', '1', 'baz' ]
25+
26+
console.log(formatJsonPointer(['f~o/o', 'bar', '1', 'baz']));
27+
// /f~0o~1o/bar/1/baz
28+
29+
console.log(unescapeComponent('~0~1'));
30+
// ~/
31+
32+
console.log(escapeComponent('~/'));
33+
// ~0~1

‎src/__tests__/find.spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import {find} from '../find';
2+
import {testFindRef} from './testFindRef';
3+
4+
testFindRef(find);

‎src/__tests__/testFindRef.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import {test, expect} from 'vitest';
2+
import {Path, Reference} from '..';
3+
import {isArrayReference, isArrayEnd} from '../find';
4+
import {parseJsonPointer} from '../util';
5+
6+
export const testFindRef = (find: (val: unknown, path: Path) => Reference) => {
7+
test('can find number root', () => {
8+
const res = find(123, []);
9+
expect(res.val).toBe(123);
10+
});
11+
12+
test('can find string root', () => {
13+
const res = find('foo', []);
14+
expect(res.val).toBe('foo');
15+
});
16+
17+
test('can find key in object', () => {
18+
const res = find({foo: 'bar'}, ['foo']);
19+
expect(res.val).toBe('bar');
20+
});
21+
22+
test('returns container object and key', () => {
23+
const res = find({foo: {bar: {baz: 'qux', a: 1}}}, ['foo', 'bar', 'baz']);
24+
expect(res).toEqual({
25+
val: 'qux',
26+
obj: {baz: 'qux', a: 1},
27+
key: 'baz',
28+
});
29+
});
30+
31+
test('can reference simple object key', () => {
32+
const doc = {a: 123};
33+
const path = parseJsonPointer('/a');
34+
const res = find(doc, path);
35+
expect(res).toEqual({
36+
val: 123,
37+
obj: {a: 123},
38+
key: 'a',
39+
});
40+
});
41+
42+
test('throws when referencing missing key with multiple steps', () => {
43+
const doc = {a: 123};
44+
const path = parseJsonPointer('/b/c');
45+
expect(() => find(doc, path)).toThrow(new Error('NOT_FOUND'));
46+
});
47+
48+
test('can reference array element', () => {
49+
const doc = {a: {b: [1, 2, 3]}};
50+
const path = parseJsonPointer('/a/b/1');
51+
const res = find(doc, path);
52+
expect(res).toEqual({
53+
val: 2,
54+
obj: [1, 2, 3],
55+
key: 1,
56+
});
57+
});
58+
59+
test('can reference end of array', () => {
60+
const doc = {a: {b: [1, 2, 3]}};
61+
const path = parseJsonPointer('/a/b/-');
62+
const ref = find(doc, path);
63+
expect(ref).toEqual({
64+
val: undefined,
65+
obj: [1, 2, 3],
66+
key: 3,
67+
});
68+
expect(isArrayReference(ref)).toBe(true);
69+
if (isArrayReference(ref)) expect(isArrayEnd(ref)).toBe(true);
70+
});
71+
72+
test('throws when pointing past array boundary', () => {
73+
const doc = {a: {b: [1, 2, 3]}};
74+
const path = parseJsonPointer('/a/b/-1');
75+
expect(() => find(doc, path)).toThrow(new Error('INVALID_INDEX'));
76+
});
77+
78+
test('can point one element past array boundary', () => {
79+
const doc = {a: {b: [1, 2, 3]}};
80+
const path = parseJsonPointer('/a/b/3');
81+
const ref = find(doc, path);
82+
expect(ref).toEqual({
83+
val: undefined,
84+
obj: [1, 2, 3],
85+
key: 3,
86+
});
87+
expect(isArrayReference(ref)).toBe(true);
88+
if (isArrayReference(ref)) expect(isArrayEnd(ref)).toBe(true);
89+
});
90+
91+
test('can reference missing object key', () => {
92+
const doc = {foo: 123};
93+
const path = parseJsonPointer('/bar');
94+
const ref = find(doc, path);
95+
expect(ref).toEqual({
96+
val: undefined,
97+
obj: {foo: 123},
98+
key: 'bar',
99+
});
100+
});
101+
102+
test('can reference missing array key withing bounds', () => {
103+
const doc = {foo: 123, bar: [1, 2, 3]};
104+
const path = parseJsonPointer('/bar/3');
105+
const ref = find(doc, path);
106+
expect(ref).toEqual({
107+
val: undefined,
108+
obj: [1, 2, 3],
109+
key: 3,
110+
});
111+
});
112+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {test, expect} from 'vitest';
2+
import {escapeComponent} from '../util';
3+
4+
test('string without escaped characters as is', () => {
5+
const res = escapeComponent('foobar');
6+
expect(res).toBe('foobar');
7+
});
8+
9+
test('replaces special characters', () => {
10+
expect(escapeComponent('foo~/')).toBe('foo~0~1');
11+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import {test, expect} from 'vitest';
2+
import {formatJsonPointer} from '../util';
3+
4+
test('returns path without escaped characters parsed into array', () => {
5+
const res = formatJsonPointer(['foo', 'bar']);
6+
expect(res).toBe('/foo/bar');
7+
});
8+
9+
test('empty string elements add trailing slashes', () => {
10+
const res = formatJsonPointer(['foo', '', '', '']);
11+
expect(res).toBe('/foo///');
12+
});
13+
14+
test('array with single empty string results into root element', () => {
15+
const res = formatJsonPointer([]);
16+
expect(res).toBe('');
17+
});
18+
19+
test('two empty strings result in a single slash "/"', () => {
20+
const res = formatJsonPointer(['']);
21+
expect(res).toBe('/');
22+
});
23+
24+
test('escapes special characters', () => {
25+
const res = formatJsonPointer(['a~b', 'c/d', '1']);
26+
expect(res).toBe('/a~0b/c~1d/1');
27+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import {test, expect} from 'vitest';
2+
import {isChild} from '../util';
3+
4+
test('returns false if parent path is longer than child path', () => {
5+
const res = isChild(['', 'foo', 'bar', 'baz'], ['', 'foo']);
6+
expect(res).toBe(false);
7+
});
8+
9+
test('returns true for real child', () => {
10+
const res = isChild(['', 'foo'], ['', 'foo', 'bar', 'baz']);
11+
expect(res).toBe(true);
12+
});
13+
14+
test('returns false for different root steps', () => {
15+
const res = isChild(['', 'foo'], ['', 'foo2', 'bar', 'baz']);
16+
expect(res).toBe(false);
17+
});
18+
19+
test('returns false for adjacent paths', () => {
20+
const res = isChild(['', 'foo', 'baz'], ['', 'foo', 'bar']);
21+
expect(res).toBe(false);
22+
});
23+
24+
test('returns false for two roots', () => {
25+
const res = isChild([''], ['']);
26+
expect(res).toBe(false);
27+
});
28+
29+
test('always returns true when parent is root and child is not', () => {
30+
const res = isChild([''], ['', 'a', 'b', 'c', '1', '2', '3']);
31+
expect(res).toBe(true);
32+
});

‎src/__tests__/util.parent.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import {test, expect} from 'vitest';
2+
import {parent} from '../util';
3+
4+
test('returns parent path', () => {
5+
expect(parent(['foo', 'bar', 'baz'])).toEqual(['foo', 'bar']);
6+
expect(parent(['foo', 'bar'])).toEqual(['foo']);
7+
expect(parent(['foo'])).toEqual([]);
8+
});
9+
10+
test('throws when path has no parent', () => {
11+
expect(() => parent([])).toThrow(new Error('NO_PARENT'));
12+
});

0 commit comments

Comments
 (0)