@hamak/navigation-utils 0.4.0
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/README.md +82 -0
- package/dist/es2015/filesystem/fs-node-abstract.types.js +1 -0
- package/dist/es2015/index.js +9 -0
- package/dist/es2015/itinerary/hyper-layer-node.js +21 -0
- package/dist/es2015/itinerary/itinerary.js +276 -0
- package/dist/es2015/itinerary/itinerary.spec.js +296 -0
- package/dist/es2015/itinerary/stack.js +47 -0
- package/dist/es2015/itinerary/stack.spec.js +35 -0
- package/dist/es2015/path/pathway-resolver.js +31 -0
- package/dist/es2015/path/pathway.js +206 -0
- package/dist/filesystem/fs-node-abstract.types.d.ts +34 -0
- package/dist/filesystem/fs-node-abstract.types.d.ts.map +1 -0
- package/dist/filesystem/fs-node-abstract.types.js +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/itinerary/hyper-layer-node.d.ts +12 -0
- package/dist/itinerary/hyper-layer-node.d.ts.map +1 -0
- package/dist/itinerary/hyper-layer-node.js +21 -0
- package/dist/itinerary/itinerary.d.ts +86 -0
- package/dist/itinerary/itinerary.d.ts.map +1 -0
- package/dist/itinerary/itinerary.js +275 -0
- package/dist/itinerary/itinerary.spec.d.ts +2 -0
- package/dist/itinerary/itinerary.spec.d.ts.map +1 -0
- package/dist/itinerary/itinerary.spec.js +294 -0
- package/dist/itinerary/stack.d.ts +20 -0
- package/dist/itinerary/stack.d.ts.map +1 -0
- package/dist/itinerary/stack.js +47 -0
- package/dist/itinerary/stack.spec.d.ts +2 -0
- package/dist/itinerary/stack.spec.d.ts.map +1 -0
- package/dist/itinerary/stack.spec.js +34 -0
- package/dist/path/pathway-resolver.d.ts +20 -0
- package/dist/path/pathway-resolver.d.ts.map +1 -0
- package/dist/path/pathway-resolver.js +31 -0
- package/dist/path/pathway.d.ts +84 -0
- package/dist/path/pathway.d.ts.map +1 -0
- package/dist/path/pathway.js +206 -0
- package/package.json +41 -0
- package/src/filesystem/fs-node-abstract.types.ts +34 -0
- package/src/index.ts +11 -0
- package/src/itinerary/hyper-layer-node.ts +25 -0
- package/src/itinerary/itinerary.spec.ts +388 -0
- package/src/itinerary/itinerary.ts +363 -0
- package/src/itinerary/stack.spec.ts +46 -0
- package/src/itinerary/stack.ts +62 -0
- package/src/path/pathway-resolver.ts +36 -0
- package/src/path/pathway.ts +232 -0
- package/tsconfig.es2015.json +23 -0
- package/tsconfig.json +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# @hamak/navigation-utils
|
|
2
|
+
|
|
3
|
+
Path and data structure navigation utilities for the app-framework ecosystem.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This package provides utilities for:
|
|
8
|
+
- **Path manipulation** - Normalize, resolve, and navigate filesystem-like paths
|
|
9
|
+
- **Data structure navigation** - Navigate deep object/array structures using itineraries
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
### Pathway - Path Manipulation
|
|
14
|
+
|
|
15
|
+
The `Pathway` class provides a robust way to manipulate filesystem-like paths:
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { Pathway } from '@hamak/navigation-utils';
|
|
19
|
+
|
|
20
|
+
// Create pathways
|
|
21
|
+
const path1 = new Pathway('/users/documents');
|
|
22
|
+
const path2 = Pathway.of('projects/readme.md');
|
|
23
|
+
|
|
24
|
+
// Resolve paths
|
|
25
|
+
const resolved = path1.resolve(path2); // /users/documents/projects/readme.md
|
|
26
|
+
|
|
27
|
+
// Get parent
|
|
28
|
+
const parent = path1.getParent(); // /users
|
|
29
|
+
|
|
30
|
+
// Check relationships
|
|
31
|
+
path1.isDescendant('/users/documents/projects'); // true
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Itinerary - Data Structure Navigation
|
|
35
|
+
|
|
36
|
+
Navigate complex object/array structures using itinerary steps:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { itineraryOf, navigate, propertyStep, positionStep } from '@hamak/navigation-utils';
|
|
40
|
+
|
|
41
|
+
const data = {
|
|
42
|
+
users: [
|
|
43
|
+
{ id: 1, name: 'Alice' },
|
|
44
|
+
{ id: 2, name: 'Bob' }
|
|
45
|
+
]
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Create itinerary: users -> [1] -> name
|
|
49
|
+
const itinerary = itineraryOf('users', 1, 'name');
|
|
50
|
+
|
|
51
|
+
// Navigate
|
|
52
|
+
const result = navigate(data, itinerary); // 'Bob'
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm install @hamak/navigation-utils
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## API Reference
|
|
62
|
+
|
|
63
|
+
### Pathway
|
|
64
|
+
|
|
65
|
+
- `Pathway.of(path)` - Factory method to create a Pathway
|
|
66
|
+
- `pathway.resolve(otherPath)` - Resolve with another path
|
|
67
|
+
- `pathway.relativize(otherPath)` - Get relative path
|
|
68
|
+
- `pathway.getParent()` - Get parent directory
|
|
69
|
+
- `pathway.isAbsolute()` - Check if absolute
|
|
70
|
+
- `pathway.isDescendant(otherPath)` - Check if descendant
|
|
71
|
+
|
|
72
|
+
### Itinerary
|
|
73
|
+
|
|
74
|
+
- `itineraryOf(...segments)` - Create itinerary from segments
|
|
75
|
+
- `navigate(from, itinerary)` - Navigate data structure
|
|
76
|
+
- `propertyStep(name)` - Create property step
|
|
77
|
+
- `positionStep(index)` - Create position step
|
|
78
|
+
- `lookupStep(criteria)` - Create lookup step
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Path utilities
|
|
2
|
+
export * from './path/pathway';
|
|
3
|
+
export * from './path/pathway-resolver';
|
|
4
|
+
// Itinerary utilities
|
|
5
|
+
export * from './itinerary/itinerary';
|
|
6
|
+
export * from './itinerary/stack';
|
|
7
|
+
export * from './itinerary/hyper-layer-node';
|
|
8
|
+
// Filesystem abstractions
|
|
9
|
+
export * from './filesystem/fs-node-abstract.types';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function hyper(data, children) {
|
|
2
|
+
return { data, children };
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Utility function that create Overlay which mirror original object
|
|
6
|
+
* @param o
|
|
7
|
+
*/
|
|
8
|
+
export function hyperReflect(o) {
|
|
9
|
+
if (o === null || o === undefined) {
|
|
10
|
+
return hyper(o);
|
|
11
|
+
}
|
|
12
|
+
else if (Array.isArray(o)) {
|
|
13
|
+
return hyper(o, o.map(e => hyperReflect(e)));
|
|
14
|
+
}
|
|
15
|
+
else if (typeof o === "object") {
|
|
16
|
+
return hyper(o, Object.entries(o).reduce((acc, [k, v]) => (Object.assign(Object.assign({}, acc), { [k]: hyperReflect(v) })), {}));
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
return hyper(o);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import stack from "./stack";
|
|
2
|
+
export function constructiveItinerary(steps) {
|
|
3
|
+
const segments = [];
|
|
4
|
+
const prototypes = [];
|
|
5
|
+
for (let i = 0; i < steps.length; i += 2) {
|
|
6
|
+
const segment = steps[i];
|
|
7
|
+
const prototype = steps[i + 1];
|
|
8
|
+
switch (typeof segment) {
|
|
9
|
+
case "string":
|
|
10
|
+
case "number":
|
|
11
|
+
{
|
|
12
|
+
const step = typeof segment === "number" ? positionStep(segment) : propertyStep(segment);
|
|
13
|
+
switch (typeof prototype) {
|
|
14
|
+
case "object":
|
|
15
|
+
{ // including Array
|
|
16
|
+
segments.push(segment);
|
|
17
|
+
prototypes.push(prototype);
|
|
18
|
+
}
|
|
19
|
+
break;
|
|
20
|
+
default: {
|
|
21
|
+
throw new Error(`Expecting array or object but got : '${typeof prototype}'`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
break;
|
|
26
|
+
default: {
|
|
27
|
+
throw new Error(`Expecting string or number but gor : '${typeof segment}'`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
itinerary: itineraryOf(...segments),
|
|
33
|
+
prototypes: stack.fromArray(prototypes)
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function areSameItineraryStep(step1, step2) {
|
|
37
|
+
var _a, _b, _c;
|
|
38
|
+
if (step1 == undefined || step2 == undefined) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
else if (step1.type !== step2.type) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
switch (step1.type) {
|
|
46
|
+
case "property": return step2.type === "property" && step1.propertyName === step2.propertyName;
|
|
47
|
+
case "position": return step2.type === "position" && step1.position === step2.position;
|
|
48
|
+
case "lookup": {
|
|
49
|
+
return step2.type === "lookup"
|
|
50
|
+
&& ((_a = step1.keys) === null || _a === void 0 ? void 0 : _a.length) === ((_b = step2.keys) === null || _b === void 0 ? void 0 : _b.length)
|
|
51
|
+
&& ((_c = step1.keys) === null || _c === void 0 ? void 0 : _c.every(pv1 => step2.keys.some(pv2 => areSamePropertyValuePair(pv1, pv2))));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function areSamePropertyValuePair(p1, p2) {
|
|
57
|
+
return p1.propertyName === p2.propertyName
|
|
58
|
+
&& p1.propertyValue === p2.propertyValue;
|
|
59
|
+
}
|
|
60
|
+
export function itineraryOf(...args) {
|
|
61
|
+
return args.reduce((parent, e) => {
|
|
62
|
+
const value = typeof e === 'string' ? propertyStep(e) : positionStep(e);
|
|
63
|
+
return { parent, value };
|
|
64
|
+
}, undefined);
|
|
65
|
+
}
|
|
66
|
+
const dataStep = propertyStep("data");
|
|
67
|
+
const childrenStep = propertyStep("children");
|
|
68
|
+
export function itineraryOverlay(it) {
|
|
69
|
+
return { value: dataStep, parent: itineraryOverlayInner(it) };
|
|
70
|
+
}
|
|
71
|
+
function itineraryOverlayInner(it) {
|
|
72
|
+
if (it === undefined) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const { value, parent } = it;
|
|
77
|
+
return { value, parent: { value: childrenStep, parent: itineraryOverlayInner(parent) } };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export function itineraryToStepArray(itinerary) {
|
|
81
|
+
let itn = itinerary;
|
|
82
|
+
const result = [];
|
|
83
|
+
while (itn !== undefined) {
|
|
84
|
+
result.push(itn.value);
|
|
85
|
+
itn = itn.parent;
|
|
86
|
+
}
|
|
87
|
+
return result.reverse();
|
|
88
|
+
}
|
|
89
|
+
export function propertyStep(propertyName) {
|
|
90
|
+
return { type: "property", propertyName };
|
|
91
|
+
}
|
|
92
|
+
export function positionStep(position) {
|
|
93
|
+
return { type: "position", position };
|
|
94
|
+
}
|
|
95
|
+
export function lookupStep(criteria) {
|
|
96
|
+
return { type: "lookup", keys: Object.entries(criteria).map(([k, v]) => ({ propertyName: k, propertyValue: v })) };
|
|
97
|
+
}
|
|
98
|
+
export function xpathFromStack(path) {
|
|
99
|
+
if (path === undefined) {
|
|
100
|
+
return ''; // TODO may need to return '.' here, to be checked
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const { value, parent } = path;
|
|
104
|
+
const parentXPath = xpathFromStack(parent);
|
|
105
|
+
switch (value.type) {
|
|
106
|
+
case 'property': {
|
|
107
|
+
return `${parentXPath}/${value.propertyName}`;
|
|
108
|
+
}
|
|
109
|
+
case 'position': {
|
|
110
|
+
return `${parentXPath}/*[${value.position + 1}]`;
|
|
111
|
+
}
|
|
112
|
+
case 'lookup': {
|
|
113
|
+
const predicate = value.keys.map(({ propertyName, propertyValue }) => `${propertyName}=$${propertyName}`).join(' and ');
|
|
114
|
+
return `${parentXPath}/*[${predicate}]`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export function navigate(from, itinerary, prototype) {
|
|
120
|
+
if (from === undefined) {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
if (itinerary === undefined) {
|
|
124
|
+
return from;
|
|
125
|
+
}
|
|
126
|
+
const { parent, value: step } = itinerary;
|
|
127
|
+
const current = navigate(from, parent, prototype === null || prototype === void 0 ? void 0 : prototype.parent);
|
|
128
|
+
return navigateStep(current, step, prototype === null || prototype === void 0 ? void 0 : prototype.value);
|
|
129
|
+
}
|
|
130
|
+
export function navigateStep(from, step, prototype) {
|
|
131
|
+
if (from === undefined) {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
if (step === undefined) {
|
|
135
|
+
return from;
|
|
136
|
+
}
|
|
137
|
+
if (Array.isArray(from)) {
|
|
138
|
+
if (step.type === 'lookup') {
|
|
139
|
+
const stp = step;
|
|
140
|
+
const result = from.find(e => e !== undefined && stp.keys.every(({ propertyName, propertyValue }) => e[propertyName] === propertyValue));
|
|
141
|
+
if (result === undefined && prototype !== undefined) {
|
|
142
|
+
from.push(prototype);
|
|
143
|
+
return prototype;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else if (step.type === 'position') {
|
|
150
|
+
const result = from[step.position];
|
|
151
|
+
if (result === undefined && prototype !== undefined) {
|
|
152
|
+
from[step.position] = prototype;
|
|
153
|
+
return prototype;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else if (typeof from === 'object') {
|
|
161
|
+
if (step.type === 'property') {
|
|
162
|
+
const result = from === null ? undefined : from[step.propertyName];
|
|
163
|
+
if (result === undefined && prototype !== undefined && from !== null && from !== undefined) {
|
|
164
|
+
from[step.propertyName] = prototype;
|
|
165
|
+
return prototype;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
export function navigationDepth(from, itinerary, originalPath = xpathFromStack(itinerary)) {
|
|
175
|
+
if (from === undefined || from === null) {
|
|
176
|
+
return { value: undefined, path: '', originalPath };
|
|
177
|
+
}
|
|
178
|
+
if (itinerary === undefined) {
|
|
179
|
+
return { value: from, path: '', originalPath };
|
|
180
|
+
}
|
|
181
|
+
const { parent, value: step } = itinerary;
|
|
182
|
+
const current = navigationDepth(from, parent, originalPath);
|
|
183
|
+
if (current.value === undefined || current.value === null) {
|
|
184
|
+
return current;
|
|
185
|
+
}
|
|
186
|
+
const next = navigateStep(current.value, step);
|
|
187
|
+
if (next === undefined || next === null) {
|
|
188
|
+
return { value: current.value, path: xpathFromStack(parent), originalPath };
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
return { value: next, path: xpathFromStack(itinerary), originalPath };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
class OverlayNavigator {
|
|
195
|
+
navigate(from, itinerary) {
|
|
196
|
+
if (from === undefined) {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
if (itinerary === undefined) {
|
|
200
|
+
return from;
|
|
201
|
+
}
|
|
202
|
+
const { parent, value: step } = itinerary;
|
|
203
|
+
const current = this.navigate(from, parent);
|
|
204
|
+
return current === undefined ? undefined : this.navigateStep(current, step);
|
|
205
|
+
}
|
|
206
|
+
navigateStep(from, step) {
|
|
207
|
+
if (from === undefined) {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
if (step === undefined) {
|
|
211
|
+
return from;
|
|
212
|
+
}
|
|
213
|
+
if (Array.isArray(from.children)) {
|
|
214
|
+
if (step.type === 'lookup') {
|
|
215
|
+
const stp = step;
|
|
216
|
+
const result = from.children.find(e => e !== undefined && stp.keys.every(({ propertyName, propertyValue }) => { var _a; return ((_a = e.keys) === null || _a === void 0 ? void 0 : _a[propertyName]) === propertyValue; }));
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
else if (step.type === 'position') {
|
|
220
|
+
const result = from.children[step.position];
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else if (from.children !== null && typeof from.children === 'object') {
|
|
225
|
+
if (step.type === 'property') {
|
|
226
|
+
const result = from === null ? undefined : from.children[step.propertyName];
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
export const overlayNavigator = new OverlayNavigator();
|
|
234
|
+
/**
|
|
235
|
+
* Same as {navigate} but try to path missing data node using prototype parameter
|
|
236
|
+
* @param from
|
|
237
|
+
* @param itinerary
|
|
238
|
+
* @param prototype
|
|
239
|
+
*/
|
|
240
|
+
export function ensurePath(from, itinerary, prototype) {
|
|
241
|
+
return navigate(from, itinerary, prototype);
|
|
242
|
+
}
|
|
243
|
+
class AbstractPointerBuilder {
|
|
244
|
+
constructor() {
|
|
245
|
+
this.itinerary = [];
|
|
246
|
+
}
|
|
247
|
+
child(propertyName) {
|
|
248
|
+
this.itinerary.push({ type: "property", propertyName });
|
|
249
|
+
return this;
|
|
250
|
+
}
|
|
251
|
+
lookup(...criterion) {
|
|
252
|
+
criterion.forEach(([propertyName, propertyValue]) => {
|
|
253
|
+
this.itinerary.push({ type: "lookup", keys: [{ propertyName, propertyValue }] });
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
class RelativePointerBuilder extends AbstractPointerBuilder {
|
|
258
|
+
constructor(originUuid) {
|
|
259
|
+
super();
|
|
260
|
+
this.originUuid = originUuid;
|
|
261
|
+
this.type = "relative";
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
class AbsolutePointerBuilder extends AbstractPointerBuilder {
|
|
265
|
+
constructor() {
|
|
266
|
+
super();
|
|
267
|
+
this.type = "absolute";
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
export function pointer(fromId) {
|
|
271
|
+
if (fromId === undefined) {
|
|
272
|
+
return new AbsolutePointerBuilder();
|
|
273
|
+
}
|
|
274
|
+
return new RelativePointerBuilder(fromId);
|
|
275
|
+
}
|
|
276
|
+
export const ROOT = Object.freeze({ type: "absolute", itinerary: [] });
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { areSameItineraryStep, constructiveItinerary, itineraryOf, itineraryOverlay, itineraryToStepArray, navigate, navigationDepth, overlayNavigator, positionStep, propertyStep, xpathFromStack } from './itinerary';
|
|
3
|
+
import stack from './stack';
|
|
4
|
+
import { hyperReflect } from './hyper-layer-node';
|
|
5
|
+
describe('Navigate itinerary', () => {
|
|
6
|
+
it('should work', () => {
|
|
7
|
+
const itinerary = {
|
|
8
|
+
"value": {
|
|
9
|
+
"type": "property",
|
|
10
|
+
"propertyName": "schema"
|
|
11
|
+
},
|
|
12
|
+
"parent": {
|
|
13
|
+
"value": {
|
|
14
|
+
"type": "position",
|
|
15
|
+
"position": 0
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const actual = navigate(sampleData(), itinerary);
|
|
20
|
+
const expected = schemaEntry1.schema;
|
|
21
|
+
expect(actual).toBe(expected);
|
|
22
|
+
});
|
|
23
|
+
it('Conversion to array should work', () => {
|
|
24
|
+
const path = [0, 'schema'];
|
|
25
|
+
const itinerary = itineraryOf(...path);
|
|
26
|
+
const actual = itineraryToStepArray(itinerary);
|
|
27
|
+
const expected = path.map(e => typeof e === 'string' ? propertyStep(e) : positionStep(e));
|
|
28
|
+
expect(actual).toStrictEqual(expected);
|
|
29
|
+
});
|
|
30
|
+
it('Ensure path should work', () => {
|
|
31
|
+
const person = [{
|
|
32
|
+
name: "Elon MUSK",
|
|
33
|
+
}];
|
|
34
|
+
const path = [0, 'address'];
|
|
35
|
+
const itinerary = itineraryOf(...path);
|
|
36
|
+
const proto = { street: "Wall Street" };
|
|
37
|
+
const result = navigate(person, itinerary, stack.fromArray([proto]));
|
|
38
|
+
expect(result).toBe(proto);
|
|
39
|
+
expect(person[0].address).toBe(proto);
|
|
40
|
+
});
|
|
41
|
+
it('Deep navigation with constructiveItinerary should work', () => {
|
|
42
|
+
var _a;
|
|
43
|
+
const person = [{
|
|
44
|
+
name: "Guezo",
|
|
45
|
+
children: []
|
|
46
|
+
}];
|
|
47
|
+
const { itinerary, prototypes } = constructiveItinerary(["children", [], 0, { name: "Glélé" }, "children", [], 0, { name: "Béhanzin" }]);
|
|
48
|
+
const result = navigate(person, stack.concat(itineraryOf(0), itinerary), prototypes);
|
|
49
|
+
expect((_a = person[0]) === null || _a === void 0 ? void 0 : _a.name).toBe("Guezo");
|
|
50
|
+
expect(result.name).toBe("Béhanzin");
|
|
51
|
+
});
|
|
52
|
+
it("Overlay navigation should work", () => {
|
|
53
|
+
var _a;
|
|
54
|
+
const person = {
|
|
55
|
+
name: "John Doe",
|
|
56
|
+
age: 40,
|
|
57
|
+
address: {
|
|
58
|
+
street: "123 Main Street",
|
|
59
|
+
lines: ["Apt 4B", "2nd Floor"],
|
|
60
|
+
country: "USA"
|
|
61
|
+
},
|
|
62
|
+
children: [
|
|
63
|
+
{
|
|
64
|
+
name: "Jane Doe",
|
|
65
|
+
age: 10,
|
|
66
|
+
address: {
|
|
67
|
+
street: "456 Maple Avenue",
|
|
68
|
+
lines: ["House 12"],
|
|
69
|
+
country: "USA"
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "Jack Doe",
|
|
74
|
+
age: 8
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
};
|
|
78
|
+
const personOverlay = hyperReflect(person);
|
|
79
|
+
const itinerary = itineraryOf("children", 0, "address", "lines", 0);
|
|
80
|
+
expect(navigate(person, itinerary)).toBe("House 12");
|
|
81
|
+
expect(navigate(personOverlay, itineraryOverlay(itinerary))).toBe("House 12");
|
|
82
|
+
expect((_a = overlayNavigator.navigate(personOverlay, itinerary)) === null || _a === void 0 ? void 0 : _a.data).toBe("House 12");
|
|
83
|
+
});
|
|
84
|
+
it("Overlay itinerary rewrite should work", () => {
|
|
85
|
+
const toOverlay = (...path) => {
|
|
86
|
+
return xpathFromStack(itineraryOverlay(itineraryOf(...path)));
|
|
87
|
+
};
|
|
88
|
+
expect(toOverlay()).toBe("/data");
|
|
89
|
+
expect(toOverlay("name")).toBe("/children/name/data");
|
|
90
|
+
expect(toOverlay("persons", 0)).toBe("/children/persons/children/*[1]/data");
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
const schemaEntry1 = {
|
|
94
|
+
"id": "schema:User",
|
|
95
|
+
"schema": {
|
|
96
|
+
"schemaNodeType": "object",
|
|
97
|
+
"operator": false,
|
|
98
|
+
"facets": {},
|
|
99
|
+
"properties": [
|
|
100
|
+
{
|
|
101
|
+
"uuid": "172302fe-29de-462a-9e27-053463b6d118",
|
|
102
|
+
"schemaNodeType": "property",
|
|
103
|
+
"facets": {},
|
|
104
|
+
"name": "uuid",
|
|
105
|
+
"valueSchema": {
|
|
106
|
+
"schemaNodeType": "scalar",
|
|
107
|
+
"operator": false,
|
|
108
|
+
"facets": {},
|
|
109
|
+
"type": "string"
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"uuid": "733675e6-543e-4013-a071-a9a45145321b",
|
|
114
|
+
"schemaNodeType": "property",
|
|
115
|
+
"facets": {},
|
|
116
|
+
"name": "name",
|
|
117
|
+
"valueSchema": {
|
|
118
|
+
"schemaNodeType": "scalar",
|
|
119
|
+
"operator": false,
|
|
120
|
+
"facets": {},
|
|
121
|
+
"type": "string"
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"uuid": "8376b829-459a-4b40-b2f9-71a6aea506e5",
|
|
126
|
+
"schemaNodeType": "property",
|
|
127
|
+
"facets": {},
|
|
128
|
+
"name": "firstName",
|
|
129
|
+
"valueSchema": {
|
|
130
|
+
"schemaNodeType": "scalar",
|
|
131
|
+
"operator": false,
|
|
132
|
+
"facets": {},
|
|
133
|
+
"type": "string"
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"uuid": "3c39bfc4-90ae-4525-9366-47c7866265f3",
|
|
138
|
+
"schemaNodeType": "property",
|
|
139
|
+
"facets": {},
|
|
140
|
+
"name": "lastName",
|
|
141
|
+
"valueSchema": {
|
|
142
|
+
"schemaNodeType": "scalar",
|
|
143
|
+
"operator": false,
|
|
144
|
+
"facets": {},
|
|
145
|
+
"type": "string"
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"uuid": "a5f05908-8a9f-4879-8dc9-f5214a6aa693",
|
|
150
|
+
"schemaNodeType": "property",
|
|
151
|
+
"facets": {},
|
|
152
|
+
"name": "age",
|
|
153
|
+
"valueSchema": {
|
|
154
|
+
"schemaNodeType": "scalar",
|
|
155
|
+
"operator": false,
|
|
156
|
+
"facets": {},
|
|
157
|
+
"type": "integer"
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
function sampleData() {
|
|
164
|
+
return [
|
|
165
|
+
schemaEntry1,
|
|
166
|
+
{
|
|
167
|
+
"id": "schema:Car",
|
|
168
|
+
"schema": {
|
|
169
|
+
"schemaNodeType": "object",
|
|
170
|
+
"operator": false,
|
|
171
|
+
"facets": {},
|
|
172
|
+
"properties": [
|
|
173
|
+
{
|
|
174
|
+
"uuid": "50fa33fe-03b4-45f2-b5df-da00e742bb89",
|
|
175
|
+
"schemaNodeType": "property",
|
|
176
|
+
"facets": {},
|
|
177
|
+
"name": "uuid",
|
|
178
|
+
"valueSchema": {
|
|
179
|
+
"schemaNodeType": "scalar",
|
|
180
|
+
"operator": false,
|
|
181
|
+
"facets": {},
|
|
182
|
+
"type": "string"
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"uuid": "6ff05784-dde5-4a70-a57b-d93cba8afb00",
|
|
187
|
+
"schemaNodeType": "property",
|
|
188
|
+
"facets": {},
|
|
189
|
+
"name": "name",
|
|
190
|
+
"valueSchema": {
|
|
191
|
+
"schemaNodeType": "scalar",
|
|
192
|
+
"operator": false,
|
|
193
|
+
"facets": {},
|
|
194
|
+
"type": "string"
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"uuid": "a8dae1ce-a82f-46f4-a2a0-4fc177988e3c",
|
|
199
|
+
"schemaNodeType": "property",
|
|
200
|
+
"facets": {},
|
|
201
|
+
"name": "brand",
|
|
202
|
+
"valueSchema": {
|
|
203
|
+
"schemaNodeType": "scalar",
|
|
204
|
+
"operator": false,
|
|
205
|
+
"facets": {},
|
|
206
|
+
"type": "string"
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"uuid": "85342429-673f-420e-b0d0-7e019e7bea2a",
|
|
211
|
+
"schemaNodeType": "property",
|
|
212
|
+
"facets": {},
|
|
213
|
+
"name": "color",
|
|
214
|
+
"valueSchema": {
|
|
215
|
+
"schemaNodeType": "scalar",
|
|
216
|
+
"operator": false,
|
|
217
|
+
"facets": {},
|
|
218
|
+
"type": "string"
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
]
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
// Unit tests for areSameItineraryStep
|
|
227
|
+
describe('areSameItineraryStep', () => {
|
|
228
|
+
it('returns true for two position steps with the same position', () => {
|
|
229
|
+
expect(areSameItineraryStep(positionStep(3), positionStep(3))).toBe(true);
|
|
230
|
+
});
|
|
231
|
+
it('returns false for two position steps with different positions', () => {
|
|
232
|
+
expect(areSameItineraryStep(positionStep(2), positionStep(5))).toBe(false);
|
|
233
|
+
});
|
|
234
|
+
it('returns true for two property steps with the same propertyName', () => {
|
|
235
|
+
expect(areSameItineraryStep(propertyStep('foo'), propertyStep('foo'))).toBe(true);
|
|
236
|
+
});
|
|
237
|
+
it('returns false for two property steps with different propertyNames', () => {
|
|
238
|
+
expect(areSameItineraryStep(propertyStep('foo'), propertyStep('bar'))).toBe(false);
|
|
239
|
+
});
|
|
240
|
+
it('returns false for steps of different types', () => {
|
|
241
|
+
expect(areSameItineraryStep(positionStep(1), propertyStep('1'))).toBe(false);
|
|
242
|
+
});
|
|
243
|
+
it('returns false if one or both steps are null or undefined', () => {
|
|
244
|
+
expect(areSameItineraryStep(positionStep(1), null)).toBe(false);
|
|
245
|
+
expect(areSameItineraryStep(undefined, propertyStep('foo'))).toBe(false);
|
|
246
|
+
expect(areSameItineraryStep(undefined, undefined)).toBe(false);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
describe('navigationDepth with itineraryOf', () => {
|
|
250
|
+
it('should return root when itinerary is undefined', () => {
|
|
251
|
+
const obj = { a: 1 };
|
|
252
|
+
const result = navigationDepth(obj, undefined);
|
|
253
|
+
expect(result.value).toBe(obj);
|
|
254
|
+
expect(result.path).toBe('');
|
|
255
|
+
});
|
|
256
|
+
it('should navigate fully when path exists', () => {
|
|
257
|
+
const obj = { a: { b: { c: 42 } } };
|
|
258
|
+
const itinerary = itineraryOf('a', 'b', 'c');
|
|
259
|
+
const result = navigationDepth(obj, itinerary);
|
|
260
|
+
expect(result.value).toBe(42);
|
|
261
|
+
expect(result.path).toBe('/a/b/c');
|
|
262
|
+
});
|
|
263
|
+
it('should stop at last defined property (null)', () => {
|
|
264
|
+
const obj = { a: { b: null } };
|
|
265
|
+
const itinerary = itineraryOf('a', 'b', 'c');
|
|
266
|
+
const result = navigationDepth(obj, itinerary);
|
|
267
|
+
expect(result.value).toEqual({ b: null });
|
|
268
|
+
expect(result.path).toBe('/a/b');
|
|
269
|
+
});
|
|
270
|
+
it('should stop when root is undefined', () => {
|
|
271
|
+
const result = navigationDepth(undefined, itineraryOf('a', 'b'));
|
|
272
|
+
expect(result.value).toBeUndefined();
|
|
273
|
+
expect(result.path).toBe('');
|
|
274
|
+
});
|
|
275
|
+
it('should navigate inside array with position step', () => {
|
|
276
|
+
const obj = { list: [{ id: 1 }, { id: 2 }] };
|
|
277
|
+
const itinerary = itineraryOf('list', 1);
|
|
278
|
+
const result = navigationDepth(obj, itinerary);
|
|
279
|
+
expect(result.value).toEqual({ id: 2 });
|
|
280
|
+
expect(result.path).toBe('/list/*[2]');
|
|
281
|
+
});
|
|
282
|
+
it('should stop when lookup fails', () => {
|
|
283
|
+
const obj = { list: [{ id: 1 }, { id: 2 }] };
|
|
284
|
+
const lookupStep = {
|
|
285
|
+
type: 'lookup',
|
|
286
|
+
keys: [{ propertyName: 'id', propertyValue: 3 }]
|
|
287
|
+
};
|
|
288
|
+
const itinerary = {
|
|
289
|
+
value: lookupStep,
|
|
290
|
+
parent: itineraryOf('list')
|
|
291
|
+
};
|
|
292
|
+
const result = navigationDepth(obj, itinerary);
|
|
293
|
+
expect(result.value).toEqual(obj.list);
|
|
294
|
+
expect(result.path).toBe('/list');
|
|
295
|
+
});
|
|
296
|
+
});
|