@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.
Files changed (49) hide show
  1. package/README.md +82 -0
  2. package/dist/es2015/filesystem/fs-node-abstract.types.js +1 -0
  3. package/dist/es2015/index.js +9 -0
  4. package/dist/es2015/itinerary/hyper-layer-node.js +21 -0
  5. package/dist/es2015/itinerary/itinerary.js +276 -0
  6. package/dist/es2015/itinerary/itinerary.spec.js +296 -0
  7. package/dist/es2015/itinerary/stack.js +47 -0
  8. package/dist/es2015/itinerary/stack.spec.js +35 -0
  9. package/dist/es2015/path/pathway-resolver.js +31 -0
  10. package/dist/es2015/path/pathway.js +206 -0
  11. package/dist/filesystem/fs-node-abstract.types.d.ts +34 -0
  12. package/dist/filesystem/fs-node-abstract.types.d.ts.map +1 -0
  13. package/dist/filesystem/fs-node-abstract.types.js +1 -0
  14. package/dist/index.d.ts +7 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +9 -0
  17. package/dist/itinerary/hyper-layer-node.d.ts +12 -0
  18. package/dist/itinerary/hyper-layer-node.d.ts.map +1 -0
  19. package/dist/itinerary/hyper-layer-node.js +21 -0
  20. package/dist/itinerary/itinerary.d.ts +86 -0
  21. package/dist/itinerary/itinerary.d.ts.map +1 -0
  22. package/dist/itinerary/itinerary.js +275 -0
  23. package/dist/itinerary/itinerary.spec.d.ts +2 -0
  24. package/dist/itinerary/itinerary.spec.d.ts.map +1 -0
  25. package/dist/itinerary/itinerary.spec.js +294 -0
  26. package/dist/itinerary/stack.d.ts +20 -0
  27. package/dist/itinerary/stack.d.ts.map +1 -0
  28. package/dist/itinerary/stack.js +47 -0
  29. package/dist/itinerary/stack.spec.d.ts +2 -0
  30. package/dist/itinerary/stack.spec.d.ts.map +1 -0
  31. package/dist/itinerary/stack.spec.js +34 -0
  32. package/dist/path/pathway-resolver.d.ts +20 -0
  33. package/dist/path/pathway-resolver.d.ts.map +1 -0
  34. package/dist/path/pathway-resolver.js +31 -0
  35. package/dist/path/pathway.d.ts +84 -0
  36. package/dist/path/pathway.d.ts.map +1 -0
  37. package/dist/path/pathway.js +206 -0
  38. package/package.json +41 -0
  39. package/src/filesystem/fs-node-abstract.types.ts +34 -0
  40. package/src/index.ts +11 -0
  41. package/src/itinerary/hyper-layer-node.ts +25 -0
  42. package/src/itinerary/itinerary.spec.ts +388 -0
  43. package/src/itinerary/itinerary.ts +363 -0
  44. package/src/itinerary/stack.spec.ts +46 -0
  45. package/src/itinerary/stack.ts +62 -0
  46. package/src/path/pathway-resolver.ts +36 -0
  47. package/src/path/pathway.ts +232 -0
  48. package/tsconfig.es2015.json +23 -0
  49. package/tsconfig.json +19 -0
@@ -0,0 +1,363 @@
1
+ import stack, {StackElement} from "./stack";
2
+ import {HyperLayerNode} from "./hyper-layer-node";
3
+
4
+ export type ObjectId = string | number
5
+
6
+ interface PropertyValuePair<P = string,V = any> {
7
+ propertyName : P
8
+ propertyValue : V
9
+ }
10
+
11
+ export interface ItineraryStepBase {
12
+ type : "property" | "lookup" | "position"
13
+ }
14
+
15
+ export interface PropertyStep extends ItineraryStepBase{
16
+ type : "property"
17
+ propertyName : string
18
+ }
19
+
20
+ export interface LookupStep extends ItineraryStepBase{
21
+ type : "lookup",
22
+ keys : PropertyValuePair[]
23
+ }
24
+
25
+ export interface PositionStep extends ItineraryStepBase{
26
+ type : "position",
27
+ position : number
28
+ }
29
+
30
+ export type ItineraryStep = PropertyStep | LookupStep | PositionStep
31
+
32
+ export type Itinerary = StackElement<ItineraryStep>
33
+
34
+ export interface ConstructiveItinerary {
35
+ itinerary : Itinerary | undefined
36
+ prototypes : StackElement<any> | undefined
37
+ }
38
+
39
+ export type StepValueNodeType = Array<any> | Record<string | number, any>
40
+
41
+ export function constructiveItinerary(steps : Array<string | number | StepValueNodeType>) : ConstructiveItinerary {
42
+ const segments : Array<string | number> = []
43
+ const prototypes : Array<any> = []
44
+
45
+ for (let i = 0; i < steps.length; i+=2) {
46
+ const segment = steps[i]
47
+ const prototype = steps[i+1]
48
+
49
+ switch (typeof segment) {
50
+ case "string":
51
+ case "number":{
52
+ const step = typeof segment === "number" ? positionStep(segment) : propertyStep(segment)
53
+ switch (typeof prototype) {
54
+ case "object": { // including Array
55
+ segments.push(segment)
56
+ prototypes.push(prototype)
57
+ } break
58
+ default: {
59
+ throw new Error(`Expecting array or object but got : '${typeof prototype}'`)
60
+ }
61
+ }
62
+ } break
63
+ default: {
64
+ throw new Error(`Expecting string or number but gor : '${typeof segment}'`)
65
+ }
66
+ }
67
+ }
68
+
69
+ return {
70
+ itinerary: itineraryOf(...segments),
71
+ prototypes: stack.fromArray(prototypes)
72
+ }
73
+ }
74
+
75
+ export function areSameItineraryStep(step1 : ItineraryStep | null | undefined, step2: ItineraryStep | null | undefined) : boolean {
76
+ if(step1 == undefined || step2 == undefined){
77
+ return false
78
+ } else if(step1.type !== step2.type){
79
+ return false
80
+ } else {
81
+ switch (step1.type) {
82
+ case "property": return step2.type === "property" && step1.propertyName === step2.propertyName
83
+ case "position": return step2.type === "position" && step1.position === step2.position
84
+ case "lookup": {
85
+ return step2.type === "lookup"
86
+ && step1.keys?.length === step2.keys?.length
87
+ && step1.keys?.every(
88
+ pv1 => step2.keys.some(
89
+ pv2 => areSamePropertyValuePair(pv1, pv2)
90
+ )
91
+ )
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ function areSamePropertyValuePair(p1: PropertyValuePair, p2: PropertyValuePair) {
98
+ return p1.propertyName === p2.propertyName
99
+ && p1.propertyValue === p2.propertyValue
100
+ }
101
+
102
+ export function itineraryOf(... args : (string | number)[]) : Itinerary | undefined{
103
+ return args.reduce((parent, e) => {
104
+ const value = typeof e === 'string' ? propertyStep(e) : positionStep(e)
105
+ return {parent, value}
106
+ }, undefined as StackElement<ItineraryStep> | undefined)
107
+ }
108
+
109
+ const dataStep = propertyStep("data")
110
+ const childrenStep = propertyStep("children")
111
+
112
+ export function itineraryOverlay(it : Itinerary | undefined) : Itinerary {
113
+ return {value:dataStep, parent:itineraryOverlayInner(it)}
114
+ }
115
+
116
+ function itineraryOverlayInner(it : Itinerary | undefined) : Itinerary | undefined{
117
+ if(it === undefined){
118
+ return undefined
119
+ } else {
120
+ const {value, parent} = it
121
+ return {value, parent:{value:childrenStep, parent: itineraryOverlayInner(parent)}}
122
+ }
123
+ }
124
+
125
+ export function itineraryToStepArray(itinerary :Itinerary | undefined) : ItineraryStep[] {
126
+ let itn : Itinerary | undefined = itinerary
127
+ const result : ItineraryStep[] = []
128
+ while (itn !== undefined){
129
+ result.push(itn.value)
130
+ itn = itn.parent
131
+ }
132
+
133
+ return result.reverse()
134
+ }
135
+
136
+ export function propertyStep(propertyName: string) : PropertyStep {
137
+ return {type:"property", propertyName}
138
+ }
139
+
140
+ export function positionStep(position: number) : PositionStep {
141
+ return {type:"position", position}
142
+ }
143
+
144
+ export function lookupStep(criteria: Record<string, any>) : LookupStep {
145
+ return {type:"lookup", keys:Object.entries(criteria).map(([k, v]) => ({propertyName:k, propertyValue:v}))}
146
+ }
147
+
148
+ export function xpathFromStack(path?: StackElement<ItineraryStep>) : string{
149
+ if(path === undefined){
150
+ return '' // TODO may need to return '.' here, to be checked
151
+ }else{
152
+ const {value, parent} = path
153
+ const parentXPath = xpathFromStack(parent)
154
+
155
+ switch(value.type){
156
+ case 'property' : {
157
+ return `${parentXPath}/${value.propertyName}`
158
+ }
159
+ case 'position' : {
160
+ return `${parentXPath}/*[${value.position + 1}]`
161
+ }
162
+ case 'lookup' : {
163
+ const predicate = value.keys.map(({propertyName,propertyValue}) => `${propertyName}=$${propertyName}`).join(' and ')
164
+ return `${parentXPath}/*[${predicate}]`
165
+ }
166
+ }
167
+
168
+ }
169
+ }
170
+
171
+ export function navigate(from: any, itinerary : Itinerary | undefined, prototype? : StackElement<any>) : any {
172
+ if(from === undefined){
173
+ return undefined
174
+ }
175
+ if(itinerary === undefined){
176
+ return from
177
+ }
178
+ const {parent, value : step} = itinerary
179
+
180
+ const current = navigate(from, parent, prototype?.parent)
181
+
182
+ return navigateStep(current, step, prototype?.value)
183
+ }
184
+
185
+ export function navigateStep(from: any, step : ItineraryStep, prototype? : any) : any {
186
+ if(from === undefined){
187
+ return undefined
188
+ }
189
+ if(step === undefined){
190
+ return from
191
+ }
192
+
193
+ if(Array.isArray(from)){
194
+ if(step.type === 'lookup'){
195
+ const stp = step
196
+ const result = from.find(e => e !== undefined && stp.keys.every(({propertyName, propertyValue}) => e[propertyName] === propertyValue))
197
+ if (result === undefined && prototype !== undefined){
198
+ from.push(prototype)
199
+ return prototype
200
+ } else {
201
+ return result
202
+ }
203
+ }else if(step.type === 'position'){
204
+ const result = from[step.position];
205
+ if (result === undefined && prototype !== undefined){
206
+ from[step.position] = prototype
207
+ return prototype
208
+ } else {
209
+ return result
210
+ }
211
+ }
212
+ }else if(typeof from === 'object'){
213
+ if(step.type === 'property'){
214
+ const result = from === null ? undefined : from[step.propertyName];
215
+ if(result === undefined && prototype !== undefined && from !== null && from !== undefined){
216
+ from[step.propertyName] = prototype
217
+ return prototype
218
+ } else {
219
+ return result
220
+ }
221
+ }
222
+ }
223
+
224
+ return undefined
225
+ }
226
+
227
+ export function navigationDepth(
228
+ from: any,
229
+ itinerary: Itinerary | undefined,
230
+ originalPath: string = xpathFromStack(itinerary)
231
+ ): { value: any; path: string; originalPath: string } {
232
+ if (from === undefined || from === null) {
233
+ return { value: undefined, path: '', originalPath };
234
+ }
235
+ if (itinerary === undefined) {
236
+ return { value: from, path: '', originalPath };
237
+ }
238
+
239
+ const { parent, value: step } = itinerary;
240
+ const current = navigationDepth(from, parent, originalPath);
241
+
242
+ if (current.value === undefined || current.value === null) {
243
+ return current;
244
+ }
245
+
246
+ const next = navigateStep(current.value, step);
247
+
248
+ if (next === undefined || next === null) {
249
+ return { value: current.value, path: xpathFromStack(parent), originalPath };
250
+ } else {
251
+ return { value: next, path: xpathFromStack(itinerary), originalPath };
252
+ }
253
+ }
254
+
255
+ class OverlayNavigator {
256
+
257
+ navigate<T>(from: HyperLayerNode<T>, itinerary : Itinerary | undefined) : HyperLayerNode<T> | undefined{
258
+ if(from === undefined){
259
+ return undefined
260
+ }
261
+ if(itinerary === undefined){
262
+ return from
263
+ }
264
+ const {parent, value : step} = itinerary
265
+
266
+ const current = this.navigate(from, parent)
267
+
268
+ return current === undefined ? undefined : this.navigateStep(current, step)
269
+ }
270
+
271
+ protected navigateStep<T>(from: HyperLayerNode<T>, step : ItineraryStep) : HyperLayerNode<T> | undefined {
272
+ if(from === undefined){
273
+ return undefined
274
+ }
275
+ if(step === undefined){
276
+ return from
277
+ }
278
+
279
+ if(Array.isArray(from.children)){
280
+ if(step.type === 'lookup'){
281
+ const stp = step
282
+ const result = from.children.find(e => e !== undefined && stp.keys.every(({propertyName, propertyValue}) => e.keys?.[propertyName] === propertyValue))
283
+ return result
284
+ }else if(step.type === 'position'){
285
+ const result = from.children[step.position];
286
+ return result
287
+ }
288
+ }else if(from.children !== null && typeof from.children === 'object'){
289
+ if(step.type === 'property'){
290
+ const result = from === null ? undefined : from.children[step.propertyName];
291
+ return result
292
+ }
293
+ }
294
+
295
+ return undefined
296
+ }
297
+
298
+ }
299
+
300
+ export const overlayNavigator = new OverlayNavigator()
301
+
302
+ /**
303
+ * Same as {navigate} but try to path missing data node using prototype parameter
304
+ * @param from
305
+ * @param itinerary
306
+ * @param prototype
307
+ */
308
+ export function ensurePath(from: any, itinerary : Itinerary | undefined, prototype? : any) : any {
309
+ return navigate(from, itinerary, prototype)
310
+ }
311
+
312
+ interface PointerBase {
313
+ type : "absolute" | "relative"
314
+ itinerary : ItineraryStep[]
315
+ }
316
+
317
+ export interface AbsolutePointer extends PointerBase {
318
+ type : "absolute"
319
+ }
320
+
321
+ export interface RelativePointer extends PointerBase {
322
+ type : "relative"
323
+ originUuid : ObjectId
324
+ }
325
+
326
+ abstract class AbstractPointerBuilder {
327
+ readonly itinerary : ItineraryStep[] = []
328
+ public child(propertyName : string){
329
+ this.itinerary.push({type:"property", propertyName})
330
+ return this
331
+ }
332
+ public lookup(...criterion : [string, any]){
333
+ criterion.forEach(([propertyName, propertyValue]) => {
334
+ this.itinerary.push({type:"lookup", keys:[{propertyName, propertyValue}]})
335
+ })
336
+ }
337
+ }
338
+
339
+ class RelativePointerBuilder extends AbstractPointerBuilder implements RelativePointer {
340
+ readonly type = "relative"
341
+ public constructor(public originUuid : ObjectId){
342
+ super()
343
+ }
344
+ }
345
+
346
+ class AbsolutePointerBuilder extends AbstractPointerBuilder implements AbsolutePointer {
347
+ readonly type = "absolute"
348
+ public constructor(){
349
+ super()
350
+ }
351
+ }
352
+
353
+ type Pointer = AbsolutePointer | RelativePointer
354
+
355
+ export function pointer(fromId?: ObjectId) : AbsolutePointerBuilder | RelativePointerBuilder {
356
+ if(fromId === undefined){
357
+ return new AbsolutePointerBuilder()
358
+ }
359
+
360
+ return new RelativePointerBuilder(fromId)
361
+ }
362
+
363
+ export const ROOT : AbsolutePointer = Object.freeze({type:"absolute", itinerary:[]})
@@ -0,0 +1,46 @@
1
+ import stack from "./stack";
2
+ import { describe, it, expect } from 'vitest';
3
+
4
+ describe("Core util stack", () => {
5
+ it("fromArray should work", () => {
6
+ const itinerary = stack.fromArray([1, 2, 3, 4])
7
+
8
+ expect(itinerary?.value).toBe(4)
9
+ expect(itinerary?.parent?.parent?.parent?.value).toBe(1)
10
+
11
+ expect(stack.fromArray([])).toBe(undefined)
12
+ expect(stack.fromArray(undefined)).toBe(undefined)
13
+ })
14
+
15
+ it("reduce should work", () => {
16
+ const itinerary = stack.fromArray([1, 2, 3, 4])
17
+
18
+ const result = stack.reduce(itinerary, (acc, t) => acc + t, 0);
19
+ expect(result).toBe(10)
20
+ })
21
+
22
+ it("reduce order should work", () => {
23
+ const itinerary = stack.fromArray([1, 2, 3, 4])
24
+
25
+ const result = stack.reduce(itinerary, (acc, t) => [...acc, t], [] as number[]);
26
+ expect(result).toStrictEqual([1, 2, 3, 4])
27
+ })
28
+
29
+ it("equal should work", () => {
30
+ expect(stack.equal(undefined, undefined)).toBe(false)
31
+ expect(stack.equal(null, null)).toBe(false)
32
+ expect(stack.equal(undefined, null)).toBe(false)
33
+ expect(stack.equal(null, undefined)).toBe(false)
34
+
35
+ expect(stack.equal(stack.fromArray([1, 2, 3, 4]), undefined)).toBe(false)
36
+ expect(stack.equal(stack.fromArray([1, 2, 3, 4]), null)).toBe(false)
37
+
38
+ expect(stack.equal(undefined, stack.fromArray([1, 2, 3, 4]))).toBe(false)
39
+ expect(stack.equal(null, stack.fromArray([1, 2, 3, 4]))).toBe(false)
40
+
41
+ expect(stack.equal(stack.fromArray([1]), stack.fromArray([2]))).toBe(false)
42
+ expect(stack.equal(stack.fromArray([1]), stack.fromArray([1, 2]))).toBe(false)
43
+
44
+ expect(stack.equal(stack.fromArray([1, 2, 3, 4]), stack.fromArray([1, 2, 3, 4]))).toBe(true)
45
+ })
46
+ })
@@ -0,0 +1,62 @@
1
+ export interface StackElement<T> {
2
+ value : T
3
+ parent? : StackElement<T>
4
+ }
5
+
6
+ export function head<T>(value : T) : StackElement<T> {
7
+ return {value}
8
+ }
9
+
10
+ export function push<T>(head : StackElement<T> | undefined, value : T) : StackElement<T> {
11
+ return {value, parent: head}
12
+ }
13
+
14
+ export function concat<T>(tail : StackElement<T> | undefined, stack : StackElement<T> | undefined) : StackElement<T> | undefined{
15
+ if(tail === undefined) return stack
16
+ if(stack === undefined) return tail
17
+
18
+ const {parent, value} = stack
19
+
20
+ return {value, parent:concat(tail, parent)}
21
+ }
22
+
23
+ export function fromArray<T = any>(t : T[] | undefined) : StackElement<T> | undefined {
24
+ return t?.reduce((acc, t) => push(acc, t), undefined as undefined | StackElement<T>)
25
+ }
26
+
27
+ export function reduce<T, R>(head : StackElement<T> | undefined, reducer : (r: R, t : T) => R, initial : R) : R {
28
+ if(head === undefined){
29
+ return initial
30
+ }
31
+
32
+ const {parent, value} = head
33
+ const acc = reduce(parent, reducer, initial)
34
+ return reducer(acc, value)
35
+ }
36
+
37
+ function equal<T>(a:StackElement<T> | null | undefined, b : StackElement<T> | null | undefined) : boolean {
38
+ // Not equal if both are nullish
39
+ if( a === undefined || a === null || b === undefined || b === null ){
40
+ return false
41
+ }
42
+
43
+ while (a !== undefined && b !== undefined){
44
+ if(a.value !== b.value){
45
+ return false
46
+ }
47
+
48
+ a = a.parent
49
+ b = b.parent
50
+ }
51
+
52
+ // Equal if traversed all stack at both side
53
+ if((a === undefined || a === null) && (b === undefined || b === null) ){
54
+ return true
55
+ } else {
56
+ return false
57
+ }
58
+ }
59
+
60
+ const stack = {push, head, concat, reduce, fromArray, equal}
61
+
62
+ export default stack;
@@ -0,0 +1,36 @@
1
+ import {Path, Pathway} from "./pathway";
2
+
3
+ export interface PathwayResolver<T>{
4
+ resolve(path : string | string[] | Pathway) : T | undefined
5
+ }
6
+
7
+ export class RecordPathwayResolver<T> implements PathwayResolver<T> {
8
+ constructor(readonly record : Record<string, T>) {
9
+ }
10
+
11
+ resolve(path: string | string[] | Pathway): T | undefined {
12
+ const key = Pathway.asString(path)
13
+ return this.record[key]
14
+ }
15
+ }
16
+
17
+ export class EmptyPathwayResolver<T> implements PathwayResolver<T> {
18
+ resolve(path: string | string[] | Pathway): T | undefined {
19
+ return undefined
20
+ }
21
+ }
22
+
23
+ export class RelativePathwayResolver<T> implements PathwayResolver<T> {
24
+ private relativePathway : Pathway
25
+ constructor(readonly parentResolver : PathwayResolver<T>, readonly path : Path) {
26
+ this.relativePathway = Pathway.of(path)
27
+ }
28
+ resolve(path: string | string[] | Pathway): T | undefined {
29
+ const pathway = Pathway.of(path)
30
+ if(pathway.isAbsolute()){
31
+ return this.parentResolver.resolve(path)
32
+ } else {
33
+ return this.parentResolver.resolve(this.relativePathway.resolve(pathway))
34
+ }
35
+ }
36
+ }