@safeaccess/inline 0.1.1 → 0.1.3
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/.gitattributes +1 -1
- package/CHANGELOG.md +23 -5
- package/LICENSE +1 -1
- package/README.md +79 -21
- package/dist/accessors/abstract-accessor.d.ts +24 -10
- package/dist/accessors/abstract-accessor.js +21 -8
- package/dist/accessors/abstract-integration-accessor.d.ts +22 -0
- package/dist/accessors/abstract-integration-accessor.js +23 -0
- package/dist/accessors/formats/any-accessor.d.ts +10 -8
- package/dist/accessors/formats/any-accessor.js +9 -8
- package/dist/accessors/formats/array-accessor.d.ts +2 -0
- package/dist/accessors/formats/array-accessor.js +2 -0
- package/dist/accessors/formats/env-accessor.d.ts +2 -0
- package/dist/accessors/formats/env-accessor.js +2 -0
- package/dist/accessors/formats/ini-accessor.d.ts +2 -0
- package/dist/accessors/formats/ini-accessor.js +2 -0
- package/dist/accessors/formats/json-accessor.d.ts +2 -0
- package/dist/accessors/formats/json-accessor.js +2 -0
- package/dist/accessors/formats/ndjson-accessor.d.ts +2 -0
- package/dist/accessors/formats/ndjson-accessor.js +2 -0
- package/dist/accessors/formats/object-accessor.d.ts +2 -0
- package/dist/accessors/formats/object-accessor.js +2 -0
- package/dist/accessors/formats/xml-accessor.d.ts +2 -0
- package/dist/accessors/formats/xml-accessor.js +2 -0
- package/dist/accessors/formats/yaml-accessor.d.ts +3 -1
- package/dist/accessors/formats/yaml-accessor.js +4 -2
- package/dist/cache/simple-path-cache.d.ts +51 -0
- package/dist/cache/simple-path-cache.js +72 -0
- package/dist/contracts/accessors-interface.d.ts +2 -0
- package/dist/contracts/factory-accessors-interface.d.ts +2 -0
- package/dist/contracts/filter-evaluator-interface.d.ts +28 -0
- package/dist/contracts/filter-evaluator-interface.js +1 -0
- package/dist/contracts/parse-integration-interface.d.ts +2 -0
- package/dist/contracts/parser-interface.d.ts +92 -0
- package/dist/contracts/parser-interface.js +1 -0
- package/dist/contracts/path-cache-interface.d.ts +7 -6
- package/dist/contracts/readable-accessors-interface.d.ts +11 -6
- package/dist/contracts/security-guard-interface.d.ts +2 -0
- package/dist/contracts/security-parser-interface.d.ts +2 -0
- package/dist/contracts/validatable-parser-interface.d.ts +59 -0
- package/dist/contracts/validatable-parser-interface.js +1 -0
- package/dist/contracts/writable-accessors-interface.d.ts +5 -0
- package/dist/core/accessor-factory.d.ts +124 -0
- package/dist/core/accessor-factory.js +157 -0
- package/dist/core/dot-notation-parser.d.ts +34 -5
- package/dist/core/dot-notation-parser.js +51 -10
- package/dist/core/inline-builder-accessor.d.ts +82 -0
- package/dist/core/inline-builder-accessor.js +107 -0
- package/dist/exceptions/accessor-exception.d.ts +9 -0
- package/dist/exceptions/accessor-exception.js +9 -0
- package/dist/exceptions/invalid-format-exception.d.ts +5 -0
- package/dist/exceptions/invalid-format-exception.js +5 -0
- package/dist/exceptions/parser-exception.d.ts +4 -0
- package/dist/exceptions/parser-exception.js +4 -0
- package/dist/exceptions/path-not-found-exception.d.ts +4 -0
- package/dist/exceptions/path-not-found-exception.js +4 -0
- package/dist/exceptions/readonly-violation-exception.d.ts +4 -0
- package/dist/exceptions/readonly-violation-exception.js +4 -0
- package/dist/exceptions/security-exception.d.ts +6 -0
- package/dist/exceptions/security-exception.js +6 -0
- package/dist/exceptions/unsupported-type-exception.d.ts +4 -0
- package/dist/exceptions/unsupported-type-exception.js +4 -0
- package/dist/exceptions/yaml-parse-exception.d.ts +4 -0
- package/dist/exceptions/yaml-parse-exception.js +4 -0
- package/dist/index.js +2 -1
- package/dist/inline.d.ts +26 -56
- package/dist/inline.js +43 -111
- package/dist/parser/xml-parser.js +23 -10
- package/dist/parser/yaml-parser.d.ts +54 -7
- package/dist/parser/yaml-parser.js +268 -51
- package/dist/path-query/segment-filter-parser.d.ts +142 -0
- package/dist/path-query/segment-filter-parser.js +384 -0
- package/dist/path-query/segment-parser.d.ts +98 -0
- package/dist/path-query/segment-parser.js +283 -0
- package/dist/path-query/segment-path-resolver.d.ts +149 -0
- package/dist/path-query/segment-path-resolver.js +351 -0
- package/dist/path-query/segment-type.d.ts +85 -0
- package/dist/path-query/segment-type.js +35 -0
- package/dist/security/forbidden-keys.d.ts +2 -2
- package/dist/security/forbidden-keys.js +5 -5
- package/dist/security/security-guard.d.ts +4 -1
- package/dist/security/security-guard.js +7 -2
- package/dist/security/security-parser.d.ts +10 -1
- package/dist/security/security-parser.js +10 -1
- package/dist/type-format.d.ts +2 -0
- package/dist/type-format.js +2 -0
- package/package.json +11 -3
- package/src/accessors/abstract-accessor.ts +25 -19
- package/src/accessors/abstract-integration-accessor.ts +27 -0
- package/src/accessors/formats/any-accessor.ts +11 -11
- package/src/accessors/formats/array-accessor.ts +2 -0
- package/src/accessors/formats/env-accessor.ts +2 -0
- package/src/accessors/formats/ini-accessor.ts +2 -0
- package/src/accessors/formats/json-accessor.ts +2 -0
- package/src/accessors/formats/ndjson-accessor.ts +2 -0
- package/src/accessors/formats/object-accessor.ts +2 -0
- package/src/accessors/formats/xml-accessor.ts +2 -0
- package/src/accessors/formats/yaml-accessor.ts +4 -2
- package/src/cache/simple-path-cache.ts +77 -0
- package/src/contracts/accessors-interface.ts +2 -0
- package/src/contracts/factory-accessors-interface.ts +2 -0
- package/src/contracts/filter-evaluator-interface.ts +30 -0
- package/src/contracts/parse-integration-interface.ts +2 -0
- package/src/contracts/parser-interface.ts +114 -0
- package/src/contracts/path-cache-interface.ts +8 -6
- package/src/contracts/readable-accessors-interface.ts +11 -6
- package/src/contracts/security-guard-interface.ts +2 -0
- package/src/contracts/security-parser-interface.ts +2 -0
- package/src/contracts/validatable-parser-interface.ts +64 -0
- package/src/contracts/writable-accessors-interface.ts +5 -0
- package/src/core/accessor-factory.ts +173 -0
- package/src/core/dot-notation-parser.ts +74 -11
- package/src/core/inline-builder-accessor.ts +163 -0
- package/src/exceptions/accessor-exception.ts +9 -0
- package/src/exceptions/invalid-format-exception.ts +5 -0
- package/src/exceptions/parser-exception.ts +4 -0
- package/src/exceptions/path-not-found-exception.ts +4 -0
- package/src/exceptions/readonly-violation-exception.ts +4 -0
- package/src/exceptions/security-exception.ts +6 -0
- package/src/exceptions/unsupported-type-exception.ts +4 -0
- package/src/exceptions/yaml-parse-exception.ts +4 -0
- package/src/index.ts +3 -1
- package/src/inline.ts +46 -120
- package/src/parser/xml-parser.ts +31 -10
- package/src/parser/yaml-parser.ts +310 -45
- package/src/path-query/segment-filter-parser.ts +444 -0
- package/src/path-query/segment-parser.ts +321 -0
- package/src/path-query/segment-path-resolver.ts +521 -0
- package/src/path-query/segment-type.ts +82 -0
- package/src/security/forbidden-keys.ts +5 -5
- package/src/security/security-guard.ts +10 -2
- package/src/security/security-parser.ts +18 -3
- package/src/type-format.ts +2 -0
- package/stryker.config.json +8 -10
- package/tests/accessors/abstract-accessor.test.ts +217 -0
- package/tests/accessors/abstract-integration-accessor.test.ts +37 -0
- package/tests/accessors/formats/any-accessor.test.ts +57 -0
- package/tests/accessors/formats/array-accessor.test.ts +42 -0
- package/tests/accessors/formats/env-accessor.test.ts +103 -0
- package/tests/accessors/formats/ini-accessor.test.ts +186 -0
- package/tests/accessors/{json-accessor.test.ts → formats/json-accessor.test.ts} +6 -6
- package/tests/accessors/formats/ndjson-accessor.test.ts +49 -0
- package/tests/accessors/formats/object-accessor.test.ts +172 -0
- package/tests/accessors/formats/xml-accessor.test.ts +162 -0
- package/tests/accessors/formats/yaml-accessor.test.ts +36 -0
- package/tests/cache/simple-path-cache.test.ts +168 -0
- package/tests/core/accessor-factory.test.ts +157 -0
- package/tests/core/dot-notation-parser-edge-cases.test.ts +415 -0
- package/tests/core/dot-notation-parser.test.ts +0 -288
- package/tests/core/inline-builder-accessor.test.ts +114 -0
- package/tests/exceptions/accessor-exception.test.ts +28 -0
- package/tests/exceptions/invalid-format-exception.test.ts +31 -0
- package/tests/exceptions/path-not-found-exception.test.ts +33 -0
- package/tests/exceptions/readonly-violation-exception.test.ts +35 -0
- package/tests/exceptions/security-exception.test.ts +33 -0
- package/tests/exceptions/unsupported-type-exception.test.ts +33 -0
- package/tests/exceptions/yaml-parse-exception.test.ts +38 -0
- package/tests/mocks/fake-path-cache.ts +4 -3
- package/tests/parity-from.test.ts +118 -0
- package/tests/parity.test.ts +227 -10
- package/tests/parser/xml-parser-mutations.test.ts +579 -0
- package/tests/parser/xml-parser-scanner.test.ts +379 -0
- package/tests/parser/xml-parser.test.ts +17 -330
- package/tests/parser/yaml-parser-mutations.test.ts +750 -0
- package/tests/parser/yaml-parser.test.ts +844 -18
- package/tests/path-query/segment-filter-parser-mutations.test.ts +735 -0
- package/tests/path-query/segment-filter-parser.test.ts +1091 -0
- package/tests/path-query/segment-parser-mutations.test.ts +539 -0
- package/tests/path-query/segment-parser.test.ts +606 -0
- package/tests/path-query/segment-path-resolver-mutations.test.ts +626 -0
- package/tests/path-query/segment-path-resolver.test.ts +1009 -0
- package/tests/security/security-guard-advanced.test.ts +413 -0
- package/tests/security/security-guard-forbidden-keys.test.ts +87 -0
- package/tests/security/security-guard.test.ts +8 -479
- package/tests/security/security-parser.test.ts +18 -14
- package/vitest.config.ts +3 -3
- package/benchmarks/get.bench.ts +0 -26
- package/benchmarks/parse.bench.ts +0 -41
- package/tests/accessors/accessors.test.ts +0 -1017
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PathCacheInterface } from '../../src/contracts/path-cache-interface.js';
|
|
2
|
+
import type { Segment } from '../../src/path-query/segment-type.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Fake PathCacheInterface for use in tests.
|
|
@@ -6,16 +7,16 @@ import type { PathCacheInterface } from '../../src/contracts/path-cache-interfac
|
|
|
6
7
|
* @internal
|
|
7
8
|
*/
|
|
8
9
|
export class FakePathCache implements PathCacheInterface {
|
|
9
|
-
public readonly store: Map<string,
|
|
10
|
+
public readonly store: Map<string, Segment[]> = new Map();
|
|
10
11
|
public getCallCount: number = 0;
|
|
11
12
|
public setCallCount: number = 0;
|
|
12
13
|
|
|
13
|
-
get(path: string):
|
|
14
|
+
get(path: string): Segment[] | null {
|
|
14
15
|
this.getCallCount++;
|
|
15
16
|
return this.store.get(path) ?? null;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
set(path: string, segments:
|
|
19
|
+
set(path: string, segments: Segment[]): void {
|
|
19
20
|
this.setCallCount++;
|
|
20
21
|
this.store.set(path, segments);
|
|
21
22
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { AbstractAccessor } from '../src/accessors/abstract-accessor.js';
|
|
3
|
+
import { ArrayAccessor } from '../src/accessors/formats/array-accessor.js';
|
|
4
|
+
import { ObjectAccessor } from '../src/accessors/formats/object-accessor.js';
|
|
5
|
+
import { JsonAccessor } from '../src/accessors/formats/json-accessor.js';
|
|
6
|
+
import { XmlAccessor } from '../src/accessors/formats/xml-accessor.js';
|
|
7
|
+
import { YamlAccessor } from '../src/accessors/formats/yaml-accessor.js';
|
|
8
|
+
import { IniAccessor } from '../src/accessors/formats/ini-accessor.js';
|
|
9
|
+
import { EnvAccessor } from '../src/accessors/formats/env-accessor.js';
|
|
10
|
+
import { NdjsonAccessor } from '../src/accessors/formats/ndjson-accessor.js';
|
|
11
|
+
import { AnyAccessor } from '../src/accessors/formats/any-accessor.js';
|
|
12
|
+
import { DotNotationParser } from '../src/core/dot-notation-parser.js';
|
|
13
|
+
import { InvalidFormatException } from '../src/exceptions/invalid-format-exception.js';
|
|
14
|
+
import { FakeParseIntegration } from './mocks/fake-parse-integration.js';
|
|
15
|
+
|
|
16
|
+
function makeParser(): DotNotationParser {
|
|
17
|
+
return new DotNotationParser();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe(`${AbstractAccessor.name} > from() > ArrayAccessor (parity)`, () => {
|
|
21
|
+
it('hydrates from a plain object and resolves a key', () => {
|
|
22
|
+
expect(new ArrayAccessor(makeParser()).from({ name: 'Alice' }).get('name')).toBe('Alice');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('throws InvalidFormatException for a non-object non-array input', () => {
|
|
26
|
+
expect(() => new ArrayAccessor(makeParser()).from('not-an-array')).toThrow(
|
|
27
|
+
InvalidFormatException,
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe(`${AbstractAccessor.name} > from() > ObjectAccessor (parity)`, () => {
|
|
33
|
+
it('hydrates from a JavaScript object and resolves a property', () => {
|
|
34
|
+
expect(new ObjectAccessor(makeParser()).from({ name: 'Alice' }).get('name')).toBe('Alice');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('throws InvalidFormatException for an integer input', () => {
|
|
38
|
+
expect(() => new ObjectAccessor(makeParser()).from(42)).toThrow(InvalidFormatException);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe(`${AbstractAccessor.name} > from() > JsonAccessor (parity)`, () => {
|
|
43
|
+
it('hydrates from a JSON string and resolves a key', () => {
|
|
44
|
+
expect(new JsonAccessor(makeParser()).from('{"name":"Alice"}').get('name')).toBe('Alice');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('throws InvalidFormatException for an integer input', () => {
|
|
48
|
+
expect(() => new JsonAccessor(makeParser()).from(42)).toThrow(InvalidFormatException);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe(`${AbstractAccessor.name} > from() > XmlAccessor (parity)`, () => {
|
|
53
|
+
it('hydrates from an XML string and resolves an element', () => {
|
|
54
|
+
expect(
|
|
55
|
+
new XmlAccessor(makeParser()).from('<root><name>Alice</name></root>').get('name'),
|
|
56
|
+
).toBe('Alice');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('throws InvalidFormatException for an integer input', () => {
|
|
60
|
+
expect(() => new XmlAccessor(makeParser()).from(42)).toThrow(InvalidFormatException);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe(`${AbstractAccessor.name} > from() > YamlAccessor (parity)`, () => {
|
|
65
|
+
it('hydrates from a YAML string and resolves a key', () => {
|
|
66
|
+
expect(new YamlAccessor(makeParser()).from('name: Alice\n').get('name')).toBe('Alice');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('throws InvalidFormatException for an integer input', () => {
|
|
70
|
+
expect(() => new YamlAccessor(makeParser()).from(42)).toThrow(InvalidFormatException);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe(`${AbstractAccessor.name} > from() > IniAccessor (parity)`, () => {
|
|
75
|
+
it('hydrates from an INI string and resolves a key', () => {
|
|
76
|
+
expect(new IniAccessor(makeParser()).from('[s]\nname=Alice').get('s.name')).toBe('Alice');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('throws InvalidFormatException for an integer input', () => {
|
|
80
|
+
expect(() => new IniAccessor(makeParser()).from(42)).toThrow(InvalidFormatException);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe(`${AbstractAccessor.name} > from() > EnvAccessor (parity)`, () => {
|
|
85
|
+
it('hydrates from a dotenv string and resolves a key', () => {
|
|
86
|
+
expect(new EnvAccessor(makeParser()).from('NAME=Alice\n').get('NAME')).toBe('Alice');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('throws InvalidFormatException for an integer input', () => {
|
|
90
|
+
expect(() => new EnvAccessor(makeParser()).from(42)).toThrow(InvalidFormatException);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe(`${AbstractAccessor.name} > from() > NdjsonAccessor (parity)`, () => {
|
|
95
|
+
it('hydrates from an NDJSON string and resolves via index', () => {
|
|
96
|
+
expect(new NdjsonAccessor(makeParser()).from('{"name":"Alice"}').get('0.name')).toBe(
|
|
97
|
+
'Alice',
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('throws InvalidFormatException for an integer input', () => {
|
|
102
|
+
expect(() => new NdjsonAccessor(makeParser()).from(42)).toThrow(InvalidFormatException);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe(`${AbstractAccessor.name} > from() > AnyAccessor (parity)`, () => {
|
|
107
|
+
it('hydrates via integration and resolves a key', () => {
|
|
108
|
+
const integration = new FakeParseIntegration(true, { name: 'Alice' });
|
|
109
|
+
expect(new AnyAccessor(makeParser(), integration).from('raw').get('name')).toBe('Alice');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('throws InvalidFormatException when the integration rejects the input', () => {
|
|
113
|
+
const integration = new FakeParseIntegration(false, {});
|
|
114
|
+
expect(() => new AnyAccessor(makeParser(), integration).from('bad')).toThrow(
|
|
115
|
+
InvalidFormatException,
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
});
|
package/tests/parity.test.ts
CHANGED
|
@@ -9,15 +9,51 @@ import { DotNotationParser } from '../src/core/dot-notation-parser.js';
|
|
|
9
9
|
import { SecurityGuard } from '../src/security/security-guard.js';
|
|
10
10
|
import { SecurityParser } from '../src/security/security-parser.js';
|
|
11
11
|
import { SecurityException } from '../src/exceptions/security-exception.js';
|
|
12
|
+
import { InvalidFormatException } from '../src/exceptions/invalid-format-exception.js';
|
|
13
|
+
import { FakeParseIntegration } from './mocks/fake-parse-integration.js';
|
|
12
14
|
|
|
13
|
-
describe(
|
|
15
|
+
describe(`${Inline.name} > fromAny (parity)`, () => {
|
|
16
|
+
it('throws InvalidFormatException when integration rejects the input', () => {
|
|
17
|
+
const integration = new FakeParseIntegration(false, {});
|
|
18
|
+
expect(() => Inline.fromAny('bad-input', integration)).toThrow(InvalidFormatException);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('inline integration override takes precedence over builder integration', () => {
|
|
22
|
+
const builderIntegration = new FakeParseIntegration(true, { from: 'builder' });
|
|
23
|
+
const overrideIntegration = new FakeParseIntegration(true, { from: 'override' });
|
|
24
|
+
|
|
25
|
+
const accessor = new Inline()
|
|
26
|
+
.withParserIntegration(builderIntegration)
|
|
27
|
+
.fromAny('raw', overrideIntegration);
|
|
28
|
+
|
|
29
|
+
expect(accessor.get('from')).toBe('override');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('resolves nested path through AnyAccessor', () => {
|
|
33
|
+
const integration = new FakeParseIntegration(true, { user: { name: 'Alice' } });
|
|
34
|
+
const accessor = Inline.fromAny('raw', integration);
|
|
35
|
+
expect(accessor.get('user.name')).toBe('Alice');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('throws InvalidFormatException when no integration is available', () => {
|
|
39
|
+
expect(() => Inline.fromAny('data')).toThrow(InvalidFormatException);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('throws InvalidFormatException with guidance message when no integration is set', () => {
|
|
43
|
+
expect(() => Inline.fromAny('data')).toThrow(
|
|
44
|
+
'AnyAccessor requires a ParseIntegrationInterface',
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe(`${Inline.name} > fromObject (static)`, () => {
|
|
14
50
|
it('returns correct accessor and resolves property', () => {
|
|
15
51
|
const accessor = Inline.fromObject({ user: { name: 'Alice' } });
|
|
16
52
|
expect(accessor.get('user.name')).toBe('Alice');
|
|
17
53
|
});
|
|
18
54
|
});
|
|
19
55
|
|
|
20
|
-
describe(
|
|
56
|
+
describe(`${Inline.name} > make (parity)`, () => {
|
|
21
57
|
it('creates IniAccessor by constructor', () => {
|
|
22
58
|
const accessor = Inline.make(IniAccessor, '[section]\nkey=value');
|
|
23
59
|
expect(accessor.get('section.key')).toBe('value');
|
|
@@ -39,7 +75,7 @@ describe('Inline.make (parity)', () => {
|
|
|
39
75
|
});
|
|
40
76
|
});
|
|
41
77
|
|
|
42
|
-
describe(
|
|
78
|
+
describe(`${Inline.name} > getMany (parity)`, () => {
|
|
43
79
|
it('returns multiple values keyed by path', () => {
|
|
44
80
|
const accessor = Inline.fromArray({ a: 1, b: { c: 2 } });
|
|
45
81
|
const result = accessor.getMany({ a: null, 'b.c': null });
|
|
@@ -53,7 +89,7 @@ describe('AbstractAccessor.getMany (parity)', () => {
|
|
|
53
89
|
});
|
|
54
90
|
});
|
|
55
91
|
|
|
56
|
-
describe(
|
|
92
|
+
describe(`${Inline.name} > getRaw (parity)`, () => {
|
|
57
93
|
it('stores raw input for ArrayAccessor', () => {
|
|
58
94
|
const raw = { name: 'Alice', age: 30 };
|
|
59
95
|
const accessor = Inline.fromArray(raw);
|
|
@@ -100,9 +136,9 @@ describe(`${Inline.name} > withStrictMode (parity)`, () => {
|
|
|
100
136
|
});
|
|
101
137
|
|
|
102
138
|
it('withStrictMode(true) enforces forbidden key validation for JSON', () => {
|
|
103
|
-
expect(() =>
|
|
104
|
-
|
|
105
|
-
)
|
|
139
|
+
expect(() => Inline.withStrictMode(true).fromJson('{"__proto__":"injected"}')).toThrow(
|
|
140
|
+
SecurityException,
|
|
141
|
+
);
|
|
106
142
|
});
|
|
107
143
|
|
|
108
144
|
it('strict(false) bypasses payload size validation for JSON', () => {
|
|
@@ -149,16 +185,197 @@ describe(`${Inline.name} > withStrictMode + make (parity)`, () => {
|
|
|
149
185
|
});
|
|
150
186
|
});
|
|
151
187
|
|
|
152
|
-
describe(
|
|
188
|
+
describe(`${Inline.name} > keys (parity)`, () => {
|
|
153
189
|
it('returns string keys for object-keyed data (JS and PHP both return string[])', () => {
|
|
154
190
|
const accessor = Inline.fromJson('{"name":"Alice","age":30}');
|
|
155
191
|
expect(accessor.keys()).toEqual(['name', 'age']);
|
|
156
192
|
});
|
|
157
193
|
|
|
158
194
|
it('returns numeric indices as strings for NDJSON (parity with PHP array_map strval fix)', () => {
|
|
159
|
-
// PHP: array_keys(['Alice', 'Bob']) = [0, 1] → cast → ['0', '1']
|
|
160
|
-
// JS: Object.keys({'0': {...}, '1': {...}}) = ['0', '1'] (already strings)
|
|
161
195
|
const accessor = Inline.fromNdjson('{"name":"Alice"}\n{"name":"Bob"}');
|
|
162
196
|
expect(accessor.keys()).toEqual(['0', '1']);
|
|
163
197
|
});
|
|
164
198
|
});
|
|
199
|
+
|
|
200
|
+
describe(`${Inline.name} > PathQuery > wildcard (parity)`, () => {
|
|
201
|
+
it('expands all children with a wildcard', () => {
|
|
202
|
+
const accessor = Inline.fromArray({ users: [{ name: 'Alice' }, { name: 'Bob' }] });
|
|
203
|
+
expect(accessor.get('users.*.name')).toEqual(['Alice', 'Bob']);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('returns null for a wildcard on a scalar value', () => {
|
|
207
|
+
const accessor = Inline.fromArray({ x: 42 });
|
|
208
|
+
expect(accessor.get('x.*')).toBeNull();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe(`${Inline.name} > PathQuery > filter (parity)`, () => {
|
|
213
|
+
it('filters array items that satisfy a condition', () => {
|
|
214
|
+
const accessor = Inline.fromArray({
|
|
215
|
+
items: [
|
|
216
|
+
{ name: 'Alice', age: 30 },
|
|
217
|
+
{ name: 'Bob', age: 20 },
|
|
218
|
+
{ name: 'Charlie', age: 35 },
|
|
219
|
+
],
|
|
220
|
+
});
|
|
221
|
+
expect(accessor.get('items[?age > 25].name')).toEqual(['Alice', 'Charlie']);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('returns empty array when no items match the filter', () => {
|
|
225
|
+
const accessor = Inline.fromArray({
|
|
226
|
+
items: [{ name: 'Alice', age: 10 }],
|
|
227
|
+
});
|
|
228
|
+
expect(accessor.get('items[?age > 100]')).toEqual([]);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('filters with equality on a string field', () => {
|
|
232
|
+
const accessor = Inline.fromArray({
|
|
233
|
+
users: [
|
|
234
|
+
{ role: 'admin', name: 'Alice' },
|
|
235
|
+
{ role: 'user', name: 'Bob' },
|
|
236
|
+
],
|
|
237
|
+
});
|
|
238
|
+
expect(accessor.get("users[?role == 'admin'].name")).toEqual(['Alice']);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('filters with logical AND', () => {
|
|
242
|
+
const accessor = Inline.fromArray({
|
|
243
|
+
items: [
|
|
244
|
+
{ a: 1, b: 2 },
|
|
245
|
+
{ a: 1, b: 5 },
|
|
246
|
+
{ a: 3, b: 2 },
|
|
247
|
+
],
|
|
248
|
+
});
|
|
249
|
+
expect(accessor.get('items[?a == 1 && b == 2]')).toEqual([{ a: 1, b: 2 }]);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('filters with starts_with function', () => {
|
|
253
|
+
const accessor = Inline.fromArray({
|
|
254
|
+
items: [{ name: 'Alice' }, { name: 'Anna' }, { name: 'Bob' }],
|
|
255
|
+
});
|
|
256
|
+
expect(accessor.get("items[?starts_with(@.name, 'A')].name")).toEqual(['Alice', 'Anna']);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('filters with contains function on a string', () => {
|
|
260
|
+
const accessor = Inline.fromArray({
|
|
261
|
+
items: [{ tag: 'hello-world' }, { tag: 'foo-bar' }],
|
|
262
|
+
});
|
|
263
|
+
expect(accessor.get("items[?contains(@.tag, 'world')].tag")).toEqual(['hello-world']);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe(`${Inline.name} > PathQuery > multi-key and multi-index (parity)`, () => {
|
|
268
|
+
it("selects multiple keys with ['a','b']", () => {
|
|
269
|
+
const accessor = Inline.fromArray({ a: 1, b: 2, c: 3 });
|
|
270
|
+
expect(accessor.get("['a','b']")).toEqual([1, 2]);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('selects multiple indices [0,2]', () => {
|
|
274
|
+
const accessor = Inline.fromArray({ items: ['x', 'y', 'z'] });
|
|
275
|
+
expect(accessor.get('items[0,2]')).toEqual(['x', 'z']);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('resolves a negative index [-1] as a key lookup', () => {
|
|
279
|
+
const accessor = Inline.fromArray({ items: ['a', 'b', 'c'] });
|
|
280
|
+
expect(accessor.get('items[-1]')).toBeNull();
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe(`${Inline.name} > PathQuery > slice (parity)`, () => {
|
|
285
|
+
it('slices an array [1:3]', () => {
|
|
286
|
+
const accessor = Inline.fromArray({ items: [10, 20, 30, 40, 50] });
|
|
287
|
+
expect(accessor.get('items[1:3]')).toEqual([20, 30]);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('slices with a step [0:6:2]', () => {
|
|
291
|
+
const accessor = Inline.fromArray({ items: [0, 1, 2, 3, 4, 5] });
|
|
292
|
+
expect(accessor.get('items[0:6:2]')).toEqual([0, 2, 4]);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('returns null for a slice on a scalar', () => {
|
|
296
|
+
const accessor = Inline.fromArray({ x: 'hello' });
|
|
297
|
+
expect(accessor.get('x[0:2]')).toBeNull();
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe(`${Inline.name} > PathQuery > recursive descent (parity)`, () => {
|
|
302
|
+
it('collects all values for a recursive descent key', () => {
|
|
303
|
+
const accessor = Inline.fromArray({
|
|
304
|
+
a: { name: 'top' },
|
|
305
|
+
b: { nested: { name: 'deep' } },
|
|
306
|
+
});
|
|
307
|
+
expect(accessor.get('..name')).toEqual(['top', 'deep']);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('collects values for DescentMulti with multiple keys', () => {
|
|
311
|
+
const accessor = Inline.fromArray({
|
|
312
|
+
a: { x: 1, y: 2 },
|
|
313
|
+
b: { x: 3, z: 4 },
|
|
314
|
+
});
|
|
315
|
+
const result = accessor.get("..['x','y']") as number[];
|
|
316
|
+
expect(result).toEqual([1, 3, 2]);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe(`${Inline.name} > PathQuery > projection (parity)`, () => {
|
|
321
|
+
it('projects specific fields from a map', () => {
|
|
322
|
+
const accessor = Inline.fromArray({ name: 'Alice', age: 30, city: 'NYC' });
|
|
323
|
+
expect(accessor.get('.{name,age}')).toEqual({ name: 'Alice', age: 30 });
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('projects fields with an alias', () => {
|
|
327
|
+
const accessor = Inline.fromArray({ name: 'Alice', age: 30 });
|
|
328
|
+
expect(accessor.get('.{fullName: name, years: age}')).toEqual({
|
|
329
|
+
fullName: 'Alice',
|
|
330
|
+
years: 30,
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('projects fields from a list of items', () => {
|
|
335
|
+
const accessor = Inline.fromArray({
|
|
336
|
+
users: [
|
|
337
|
+
{ name: 'Alice', age: 30 },
|
|
338
|
+
{ name: 'Bob', age: 25 },
|
|
339
|
+
],
|
|
340
|
+
});
|
|
341
|
+
expect(accessor.get('users.{name}')).toEqual([{ name: 'Alice' }, { name: 'Bob' }]);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('sets projected field to null when source key is missing', () => {
|
|
345
|
+
const accessor = Inline.fromArray({ name: 'Alice' });
|
|
346
|
+
expect(accessor.get('.{name,missing}')).toEqual({ name: 'Alice', missing: null });
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
describe(`${Inline.name} > PathQuery > bracket notation (parity)`, () => {
|
|
351
|
+
it('resolves a bracket numeric index [0]', () => {
|
|
352
|
+
const accessor = Inline.fromArray({ items: ['a', 'b', 'c'] });
|
|
353
|
+
expect(accessor.get('items[0]')).toBe('a');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("resolves a bracket quoted string key ['key']", () => {
|
|
357
|
+
const accessor = Inline.fromArray({ key: 'value' });
|
|
358
|
+
expect(accessor.get("['key']")).toBe('value');
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
describe(`${Inline.name} > PathQuery > combined queries (parity)`, () => {
|
|
363
|
+
it('chains filter with wildcard', () => {
|
|
364
|
+
const accessor = Inline.fromArray({
|
|
365
|
+
items: [{ tags: ['a', 'b'] }, { tags: ['c'] }],
|
|
366
|
+
});
|
|
367
|
+
expect(accessor.get('items.*.tags[0]')).toEqual(['a', 'c']);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('uses default value when path does not exist', () => {
|
|
371
|
+
const accessor = Inline.fromArray({ a: 1 });
|
|
372
|
+
expect(accessor.get('missing.path', 'fallback')).toBe('fallback');
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('resolves deeply nested path through multiple levels', () => {
|
|
376
|
+
const accessor = Inline.fromArray({
|
|
377
|
+
level1: { level2: { level3: { value: 'deep' } } },
|
|
378
|
+
});
|
|
379
|
+
expect(accessor.get('level1.level2.level3.value')).toBe('deep');
|
|
380
|
+
});
|
|
381
|
+
});
|