@dxos/effect 0.7.5-main.9d2a38b → 0.7.5-main.b19bfc8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/browser/index.mjs +69 -18
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +71 -18
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +69 -18
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/ast.d.ts +2 -13
- package/dist/types/src/ast.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/jsonPath.d.ts +45 -0
- package/dist/types/src/jsonPath.d.ts.map +1 -0
- package/dist/types/src/jsonPath.test.d.ts +2 -0
- package/dist/types/src/jsonPath.test.d.ts.map +1 -0
- package/package.json +8 -7
- package/src/ast.test.ts +1 -24
- package/src/ast.ts +16 -20
- package/src/index.ts +1 -0
- package/src/jsonPath.test.ts +96 -0
- package/src/jsonPath.ts +85 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../../../src/ast.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,GAAG,EAAE,MAAM,IAAI,CAAC,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../../../src/ast.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC,EAAE,MAAM,gBAAgB,CAAC;AAC9D,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAC;AAG3C,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC;AAErC,cAAc,OAAO,CAAC;AACtB,cAAc,OAAO,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC,EAAE,MAAM,gBAAgB,CAAC;AAC9D,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAC;AAG3C,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC;AAErC,cAAc,OAAO,CAAC;AACtB,cAAc,YAAY,CAAC;AAC3B,cAAc,OAAO,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Schema as S } from '@effect/schema';
|
|
2
|
+
export type JsonProp = string & {
|
|
3
|
+
__JsonPath: true;
|
|
4
|
+
__JsonProp: true;
|
|
5
|
+
};
|
|
6
|
+
export type JsonPath = string & {
|
|
7
|
+
__JsonPath: true;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* https://www.ietf.org/archive/id/draft-goessner-dispatch-jsonpath-00.html
|
|
11
|
+
*/
|
|
12
|
+
export declare const JsonPath: S.Schema<JsonPath>;
|
|
13
|
+
export declare const JsonProp: S.Schema<JsonProp>;
|
|
14
|
+
export declare const isJsonPath: (value: unknown) => value is JsonPath;
|
|
15
|
+
/**
|
|
16
|
+
* Creates a JsonPath from an array of path segments.
|
|
17
|
+
*
|
|
18
|
+
* Currently supports:
|
|
19
|
+
* - Simple property access (e.g., 'foo.bar')
|
|
20
|
+
* - Array indexing with non-negative integers (e.g., 'foo[0]')
|
|
21
|
+
* - Identifiers starting with letters, underscore, or $ (e.g., '$foo', '_bar')
|
|
22
|
+
* - Dot notation for nested properties (e.g., 'foo.bar.baz')
|
|
23
|
+
*
|
|
24
|
+
* Does not support (yet?).
|
|
25
|
+
* - Recursive descent (..)
|
|
26
|
+
* - Wildcards (*)
|
|
27
|
+
* - Array slicing
|
|
28
|
+
* - Filters
|
|
29
|
+
* - Negative indices
|
|
30
|
+
*
|
|
31
|
+
* @param path Array of string or number segments
|
|
32
|
+
* @returns Valid JsonPath or undefined if invalid
|
|
33
|
+
*/
|
|
34
|
+
export declare const createJsonPath: (path: (string | number)[]) => JsonPath;
|
|
35
|
+
/**
|
|
36
|
+
* Converts Effect validation path format (e.g. "addresses.[0].zip")
|
|
37
|
+
* to JsonPath format (e.g. "addresses[0].zip")
|
|
38
|
+
*/
|
|
39
|
+
export declare const fromEffectValidationPath: (effectPath: string) => JsonPath;
|
|
40
|
+
/**
|
|
41
|
+
* Splits a JsonPath into its constituent parts.
|
|
42
|
+
* Handles property access and array indexing.
|
|
43
|
+
*/
|
|
44
|
+
export declare const splitJsonPath: (path: JsonPath) => string[];
|
|
45
|
+
//# sourceMappingURL=jsonPath.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsonPath.d.ts","sourceRoot":"","sources":["../../../src/jsonPath.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,MAAM,IAAI,CAAC,EAAE,MAAM,gBAAgB,CAAC;AAK7C,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG;IAAE,UAAU,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,IAAI,CAAA;CAAE,CAAC;AACvE,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG;IAAE,UAAU,EAAE,IAAI,CAAA;CAAE,CAAC;AAKrD;;GAEG;AACH,eAAO,MAAM,QAAQ,EAAkD,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AAC1F,eAAO,MAAM,QAAQ,EAA0D,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AAElG,eAAO,MAAM,UAAU,UAAW,OAAO,KAAG,KAAK,IAAI,QAEpD,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,cAAc,SAAU,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,KAAG,QAa1D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,wBAAwB,eAAgB,MAAM,KAAG,QAK7D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,SAAU,QAAQ,KAAG,MAAM,EAUpD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsonPath.test.d.ts","sourceRoot":"","sources":["../../../src/jsonPath.test.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/effect",
|
|
3
|
-
"version": "0.7.5-main.
|
|
3
|
+
"version": "0.7.5-main.b19bfc8",
|
|
4
4
|
"description": "Effect utils.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"author": "info@dxos.org",
|
|
9
|
+
"type": "module",
|
|
9
10
|
"exports": {
|
|
10
11
|
".": {
|
|
11
12
|
"types": "./dist/types/src/index.d.ts",
|
|
@@ -22,18 +23,18 @@
|
|
|
22
23
|
"src"
|
|
23
24
|
],
|
|
24
25
|
"dependencies": {
|
|
25
|
-
"@dxos/invariant": "0.7.5-main.
|
|
26
|
-
"@dxos/node-std": "0.7.5-main.
|
|
27
|
-
"@dxos/util": "0.7.5-main.
|
|
26
|
+
"@dxos/invariant": "0.7.5-main.b19bfc8",
|
|
27
|
+
"@dxos/node-std": "0.7.5-main.b19bfc8",
|
|
28
|
+
"@dxos/util": "0.7.5-main.b19bfc8"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
31
|
"@effect/schema": "^0.75.5",
|
|
31
|
-
"effect": "^3.12.
|
|
32
|
-
"@dxos/log": "0.7.5-main.
|
|
32
|
+
"effect": "^3.12.3",
|
|
33
|
+
"@dxos/log": "0.7.5-main.b19bfc8"
|
|
33
34
|
},
|
|
34
35
|
"peerDependencies": {
|
|
35
36
|
"@effect/schema": "^0.75.5",
|
|
36
|
-
"effect": "^3.
|
|
37
|
+
"effect": "^3.12.3"
|
|
37
38
|
},
|
|
38
39
|
"publishConfig": {
|
|
39
40
|
"access": "public"
|
package/src/ast.test.ts
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { AST, Schema as S } from '@effect/schema';
|
|
6
|
-
import { isNone, isSome } from 'effect/Option';
|
|
7
6
|
import { describe, test } from 'vitest';
|
|
8
7
|
|
|
9
8
|
import { invariant } from '@dxos/invariant';
|
|
@@ -19,9 +18,8 @@ import {
|
|
|
19
18
|
isOption,
|
|
20
19
|
isSimpleType,
|
|
21
20
|
visit,
|
|
22
|
-
JsonPath,
|
|
23
|
-
type JsonProp,
|
|
24
21
|
} from './ast';
|
|
22
|
+
import { type JsonPath, type JsonProp } from './jsonPath';
|
|
25
23
|
|
|
26
24
|
const ZipCode = S.String.pipe(
|
|
27
25
|
S.pattern(/^\d{5}$/, {
|
|
@@ -183,25 +181,4 @@ describe('AST', () => {
|
|
|
183
181
|
);
|
|
184
182
|
}
|
|
185
183
|
});
|
|
186
|
-
|
|
187
|
-
// TODO(ZaymonFC): Update this when we settle on the right indexing syntax for arrays.
|
|
188
|
-
test('json path validation', ({ expect }) => {
|
|
189
|
-
const validatePath = S.validateOption(JsonPath);
|
|
190
|
-
|
|
191
|
-
// Valid paths.
|
|
192
|
-
expect(isSome(validatePath('foo'))).toBe(true);
|
|
193
|
-
expect(isSome(validatePath('foo.bar'))).toBe(true);
|
|
194
|
-
expect(isSome(validatePath('foo.bar.baz'))).toBe(true);
|
|
195
|
-
expect(isSome(validatePath('foo[1].bar'))).toBe(true);
|
|
196
|
-
expect(isSome(validatePath('_foo.$bar'))).toBe(true);
|
|
197
|
-
|
|
198
|
-
// Invalid paths.
|
|
199
|
-
expect(isNone(validatePath(''))).toBe(true);
|
|
200
|
-
expect(isNone(validatePath('.'))).toBe(true);
|
|
201
|
-
expect(isNone(validatePath('foo.'))).toBe(true);
|
|
202
|
-
expect(isNone(validatePath('foo..bar'))).toBe(true);
|
|
203
|
-
expect(isNone(validatePath('foo.#bar'))).toBe(true);
|
|
204
|
-
expect(isNone(validatePath('[1].bar'))).toBe(true);
|
|
205
|
-
expect(isNone(validatePath('test.[1].bar[1]'))).toBe(true);
|
|
206
|
-
});
|
|
207
184
|
});
|
package/src/ast.ts
CHANGED
|
@@ -6,7 +6,9 @@ import { AST, Schema as S } from '@effect/schema';
|
|
|
6
6
|
import { Option, pipe } from 'effect';
|
|
7
7
|
|
|
8
8
|
import { invariant } from '@dxos/invariant';
|
|
9
|
-
import {
|
|
9
|
+
import { isNonNullable } from '@dxos/util';
|
|
10
|
+
|
|
11
|
+
import { type JsonPath, type JsonProp } from './jsonPath';
|
|
10
12
|
|
|
11
13
|
//
|
|
12
14
|
// Refs
|
|
@@ -76,18 +78,6 @@ export namespace SimpleType {
|
|
|
76
78
|
// Branded types
|
|
77
79
|
//
|
|
78
80
|
|
|
79
|
-
export type JsonProp = string & { __JsonPath: true; __JsonProp: true };
|
|
80
|
-
export type JsonPath = string & { __JsonPath: true };
|
|
81
|
-
|
|
82
|
-
const PATH_REGEX = /^[a-zA-Z_$][\w$]*(?:\.[a-zA-Z_$][\w$]*|\[\d+\])*$/;
|
|
83
|
-
const PROP_REGEX = /\w+/;
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* https://www.ietf.org/archive/id/draft-goessner-dispatch-jsonpath-00.html
|
|
87
|
-
*/
|
|
88
|
-
export const JsonPath = S.NonEmptyString.pipe(S.pattern(PATH_REGEX)) as any as S.Schema<JsonPath>;
|
|
89
|
-
export const JsonProp = S.NonEmptyString.pipe(S.pattern(PROP_REGEX)) as any as S.Schema<JsonProp>;
|
|
90
|
-
|
|
91
81
|
export enum VisitResult {
|
|
92
82
|
CONTINUE = 0,
|
|
93
83
|
/**
|
|
@@ -397,11 +387,11 @@ export const getDiscriminatedType = (node: AST.AST, value: Record<string, any> =
|
|
|
397
387
|
invariant(AST.isLiteral(literal.type));
|
|
398
388
|
return literal.type.literal;
|
|
399
389
|
})
|
|
400
|
-
.filter(
|
|
390
|
+
.filter(isNonNullable);
|
|
401
391
|
|
|
402
392
|
return literals.length ? [prop, S.Literal(...literals)] : undefined;
|
|
403
393
|
})
|
|
404
|
-
.filter(
|
|
394
|
+
.filter(isNonNullable),
|
|
405
395
|
);
|
|
406
396
|
|
|
407
397
|
const schema = S.Struct(fields);
|
|
@@ -413,13 +403,19 @@ export const getDiscriminatedType = (node: AST.AST, value: Record<string, any> =
|
|
|
413
403
|
* The user is responsible for recursively calling {@link mapAst} on the AST.
|
|
414
404
|
* NOTE: Will evaluate suspended ASTs.
|
|
415
405
|
*/
|
|
416
|
-
export const mapAst = (ast: AST.AST, f: (ast: AST.AST) => AST.AST): AST.AST => {
|
|
406
|
+
export const mapAst = (ast: AST.AST, f: (ast: AST.AST, key: keyof any | undefined) => AST.AST): AST.AST => {
|
|
417
407
|
switch (ast._tag) {
|
|
418
408
|
case 'TypeLiteral':
|
|
419
409
|
return new AST.TypeLiteral(
|
|
420
410
|
ast.propertySignatures.map(
|
|
421
411
|
(prop) =>
|
|
422
|
-
new AST.PropertySignature(
|
|
412
|
+
new AST.PropertySignature(
|
|
413
|
+
prop.name,
|
|
414
|
+
f(prop.type, prop.name),
|
|
415
|
+
prop.isOptional,
|
|
416
|
+
prop.isReadonly,
|
|
417
|
+
prop.annotations,
|
|
418
|
+
),
|
|
423
419
|
),
|
|
424
420
|
ast.indexSignatures,
|
|
425
421
|
);
|
|
@@ -427,13 +423,13 @@ export const mapAst = (ast: AST.AST, f: (ast: AST.AST) => AST.AST): AST.AST => {
|
|
|
427
423
|
return AST.Union.make(ast.types.map(f), ast.annotations);
|
|
428
424
|
case 'TupleType':
|
|
429
425
|
return new AST.TupleType(
|
|
430
|
-
ast.elements.map((t) => new AST.OptionalType(f(t.type), t.isOptional, t.annotations)),
|
|
431
|
-
ast.rest.map((t) => new AST.Type(f(t.type), t.annotations)),
|
|
426
|
+
ast.elements.map((t, index) => new AST.OptionalType(f(t.type, index), t.isOptional, t.annotations)),
|
|
427
|
+
ast.rest.map((t) => new AST.Type(f(t.type, undefined), t.annotations)),
|
|
432
428
|
ast.isReadonly,
|
|
433
429
|
ast.annotations,
|
|
434
430
|
);
|
|
435
431
|
case 'Suspend': {
|
|
436
|
-
const newAst = f(ast.f());
|
|
432
|
+
const newAst = f(ast.f(), undefined);
|
|
437
433
|
return new AST.Suspend(() => newAst, ast.annotations);
|
|
438
434
|
}
|
|
439
435
|
default:
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { createJsonPath, isJsonPath, type JsonPath, splitJsonPath } from './jsonPath';
|
|
8
|
+
|
|
9
|
+
describe('createJsonPath', () => {
|
|
10
|
+
test('supported path subset', () => {
|
|
11
|
+
// Simple property access.
|
|
12
|
+
expect(createJsonPath(['foo'])).toBe('foo');
|
|
13
|
+
expect(createJsonPath(['foo', 'bar'])).toBe('foo.bar');
|
|
14
|
+
expect(createJsonPath(['names', 1, 'bar'])).toBe('names[1].bar');
|
|
15
|
+
expect(createJsonPath(['names', 1])).toBe('names[1]');
|
|
16
|
+
expect(createJsonPath(['names', 1, 'names'])).toBe('names[1].names');
|
|
17
|
+
|
|
18
|
+
// Array indexing.
|
|
19
|
+
expect(createJsonPath(['foo', 0, 'bar'])).toBe('foo[0].bar');
|
|
20
|
+
expect(createJsonPath(['items', 1])).toBe('items[1]');
|
|
21
|
+
|
|
22
|
+
// $ is valid in identifiers.
|
|
23
|
+
expect(createJsonPath(['$foo', '$bar'])).toBe('$foo.$bar');
|
|
24
|
+
expect(createJsonPath([])).toBe('');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('invalid paths', () => {
|
|
28
|
+
expect(() => createJsonPath(['123foo'])).toThrow(); // Can't start with number.
|
|
29
|
+
expect(() => createJsonPath(['foo', -1, 'bar'])).toThrow(); // No negative indices.
|
|
30
|
+
expect(() => createJsonPath(['foo', 1.5, 'bar'])).toThrow(); // No float indices.
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('path splitting', () => {
|
|
34
|
+
const cases = [
|
|
35
|
+
['foo.bar[0].baz', ['foo', 'bar', '0', 'baz']],
|
|
36
|
+
['users[1].name', ['users', '1', 'name']],
|
|
37
|
+
['data[0][1]', ['data', '0', '1']],
|
|
38
|
+
['simple.path', ['simple', 'path']],
|
|
39
|
+
['root', ['root']],
|
|
40
|
+
] as const;
|
|
41
|
+
|
|
42
|
+
cases.forEach(([input, expected]) => {
|
|
43
|
+
expect(splitJsonPath(input as JsonPath)).toEqual(expected);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('path splitting - extended cases', () => {
|
|
48
|
+
const cases = [
|
|
49
|
+
// Multiple consecutive array indices.
|
|
50
|
+
['matrix[0][1][2]', ['matrix', '0', '1', '2']],
|
|
51
|
+
// Properties with underscores and $.
|
|
52
|
+
['$_foo.bar_baz', ['$_foo', 'bar_baz']],
|
|
53
|
+
// Deep nesting.
|
|
54
|
+
['very.deep.nested[0].property.path[5]', ['very', 'deep', 'nested', '0', 'property', 'path', '5']],
|
|
55
|
+
// Single character properties.
|
|
56
|
+
['a[0].b.c', ['a', '0', 'b', 'c']],
|
|
57
|
+
// Properties containing numbers.
|
|
58
|
+
['prop123.item456[7]', ['prop123', 'item456', '7']],
|
|
59
|
+
] as const;
|
|
60
|
+
|
|
61
|
+
cases.forEach(([input, expected]) => {
|
|
62
|
+
expect(splitJsonPath(input as JsonPath)).toEqual(expected);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('invalid path formats', () => {
|
|
67
|
+
// These should return empty array or handle gracefully.
|
|
68
|
+
const invalidPaths = ['', '.', '[', ']', 'foo[].bar', 'foo[a].bar', 'foo[-1].bar'] as const;
|
|
69
|
+
|
|
70
|
+
invalidPaths.forEach((path) => {
|
|
71
|
+
expect(splitJsonPath(path as JsonPath)).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('isJsonPath validation', () => {
|
|
76
|
+
// Valid paths.
|
|
77
|
+
expect(isJsonPath('')).toBe(true);
|
|
78
|
+
expect(isJsonPath('foo')).toBe(true);
|
|
79
|
+
expect(isJsonPath('foo.bar')).toBe(true);
|
|
80
|
+
expect(isJsonPath('foo[0].bar')).toBe(true);
|
|
81
|
+
expect(isJsonPath('items[1]')).toBe(true);
|
|
82
|
+
expect(isJsonPath('$foo.$bar')).toBe(true);
|
|
83
|
+
expect(isJsonPath('matrix[0][1][2]')).toBe(true);
|
|
84
|
+
expect(isJsonPath('deep.nested[0].path')).toBe(true);
|
|
85
|
+
|
|
86
|
+
// Invalid paths.
|
|
87
|
+
expect(isJsonPath('items[0]name')).toBe(false); // Missing dot
|
|
88
|
+
expect(isJsonPath('123foo')).toBe(false); // Starts with number
|
|
89
|
+
expect(isJsonPath('foo[].bar')).toBe(false); // Empty brackets
|
|
90
|
+
expect(isJsonPath('foo[-1]')).toBe(false); // Negative index
|
|
91
|
+
expect(isJsonPath('foo[a]')).toBe(false); // Non-numeric index
|
|
92
|
+
expect(isJsonPath('.foo')).toBe(false); // Starts with dot
|
|
93
|
+
expect(isJsonPath('foo.')).toBe(false); // Ends with dot
|
|
94
|
+
expect(isJsonPath('[0]foo')).toBe(false); // Starts with bracket
|
|
95
|
+
});
|
|
96
|
+
});
|
package/src/jsonPath.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Schema as S } from '@effect/schema';
|
|
6
|
+
import { isSome } from 'effect/Option';
|
|
7
|
+
|
|
8
|
+
import { invariant } from '@dxos/invariant';
|
|
9
|
+
|
|
10
|
+
export type JsonProp = string & { __JsonPath: true; __JsonProp: true };
|
|
11
|
+
export type JsonPath = string & { __JsonPath: true };
|
|
12
|
+
|
|
13
|
+
const PATH_REGEX = /^($|[a-zA-Z_$][\w$]*(?:\.[a-zA-Z_$][\w$]*|\[\d+\](?:\.)?)*$)/;
|
|
14
|
+
const PROP_REGEX = /\w+/;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* https://www.ietf.org/archive/id/draft-goessner-dispatch-jsonpath-00.html
|
|
18
|
+
*/
|
|
19
|
+
export const JsonPath = S.String.pipe(S.pattern(PATH_REGEX)) as any as S.Schema<JsonPath>;
|
|
20
|
+
export const JsonProp = S.NonEmptyString.pipe(S.pattern(PROP_REGEX)) as any as S.Schema<JsonProp>;
|
|
21
|
+
|
|
22
|
+
export const isJsonPath = (value: unknown): value is JsonPath => {
|
|
23
|
+
return isSome(S.validateOption(JsonPath)(value));
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates a JsonPath from an array of path segments.
|
|
28
|
+
*
|
|
29
|
+
* Currently supports:
|
|
30
|
+
* - Simple property access (e.g., 'foo.bar')
|
|
31
|
+
* - Array indexing with non-negative integers (e.g., 'foo[0]')
|
|
32
|
+
* - Identifiers starting with letters, underscore, or $ (e.g., '$foo', '_bar')
|
|
33
|
+
* - Dot notation for nested properties (e.g., 'foo.bar.baz')
|
|
34
|
+
*
|
|
35
|
+
* Does not support (yet?).
|
|
36
|
+
* - Recursive descent (..)
|
|
37
|
+
* - Wildcards (*)
|
|
38
|
+
* - Array slicing
|
|
39
|
+
* - Filters
|
|
40
|
+
* - Negative indices
|
|
41
|
+
*
|
|
42
|
+
* @param path Array of string or number segments
|
|
43
|
+
* @returns Valid JsonPath or undefined if invalid
|
|
44
|
+
*/
|
|
45
|
+
export const createJsonPath = (path: (string | number)[]): JsonPath => {
|
|
46
|
+
const candidatePath = path
|
|
47
|
+
.map((p, i) => {
|
|
48
|
+
if (typeof p === 'number') {
|
|
49
|
+
return `[${p}]`;
|
|
50
|
+
} else {
|
|
51
|
+
return i === 0 ? p : `.${p}`;
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
.join('');
|
|
55
|
+
|
|
56
|
+
invariant(isJsonPath(candidatePath), `Invalid JsonPath: ${candidatePath}`);
|
|
57
|
+
return candidatePath;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Converts Effect validation path format (e.g. "addresses.[0].zip")
|
|
62
|
+
* to JsonPath format (e.g. "addresses[0].zip")
|
|
63
|
+
*/
|
|
64
|
+
export const fromEffectValidationPath = (effectPath: string): JsonPath => {
|
|
65
|
+
// Handle array notation: convert "prop.[0]" to "prop[0]"
|
|
66
|
+
const jsonPath = effectPath.replace(/\.\[(\d+)\]/g, '[$1]');
|
|
67
|
+
invariant(isJsonPath(jsonPath), `Invalid JsonPath: ${jsonPath}`);
|
|
68
|
+
return jsonPath;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Splits a JsonPath into its constituent parts.
|
|
73
|
+
* Handles property access and array indexing.
|
|
74
|
+
*/
|
|
75
|
+
export const splitJsonPath = (path: JsonPath): string[] => {
|
|
76
|
+
if (!isJsonPath(path)) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
path
|
|
82
|
+
.match(/[a-zA-Z_$][\w$]*|\[\d+\]/g)
|
|
83
|
+
?.map((part) => (part.startsWith('[') ? part.replace(/[[\]]/g, '') : part)) ?? []
|
|
84
|
+
);
|
|
85
|
+
};
|