/
objectTraversalContext.ts
151 lines (129 loc) · 4.17 KB
/
objectTraversalContext.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import get from "lodash/get"
import { type ObjectOrArray, type Visitor, BaseTreeContext } from ".."
import { Merger } from "./Merger"
import { treeUpdateStatus } from "./treeUpdateStatus"
import { type TreeContextConstructor } from "./treeContextConstructor"
import { getAncestor } from "./getAncestor"
import { numberSchema } from "../../internal/globals"
import { isObjectOrArray } from "../internal/utils"
/**
* Shares many similar methods with TreeContext, refer to that class for documentation.
*/
export class ObjectTraversalContext implements BaseTreeContext {
private _context: Record<string, any> | any[]
public readonly depth: number
private breakEmitter: () => void
public readonly path: Readonly<(string | number)[]>
private _rootContext: Record<string, any> | any[]
public constructor({
context,
depth,
breakEmitter,
path,
rootContext,
}: Required<TreeContextConstructor>) {
this._context = context
this.depth = depth
this.breakEmitter = breakEmitter
this.path = path
this._rootContext = rootContext
}
get children(): ObjectOrArray[] {
return Object.values(this._context).filter(isObjectOrArray)
}
get ancestors(): (Record<string, any> | any[])[] {
return getAncestor(this._rootContext, this.path)
}
public break(): void {
this.breakEmitter()
}
public get context(): any[] | Record<string, any> {
return this._context
}
public get rootContext(): Readonly<typeof this._rootContext> {
return this._rootContext
}
/**
* Merges the provided context with the current context.
*
* @param newContext - The new context to merge.
* @param removeExisting - Optional. Specifies whether to remove existing values in the current context. Default is false.
* @returns The status of the context merge operation.
*/
public merge(
newContext: ObjectOrArray,
removeExisting = false
): "CONTEXT_MERGED" | "NOT_CONTEXT_OR_ARRAY" {
if (!isObjectOrArray(newContext)) {
return treeUpdateStatus.NOT_CONTEXT_OR_ARRAY
}
Merger.merge(this.context, newContext, removeExisting)
return treeUpdateStatus.CONTEXT_MERGED
}
public get parent(): Record<string, any> | any[] | undefined {
if (this.isAtRoot()) {
return
}
return this.path.length === 1
? this.rootContext
: get(this.rootContext, this.path.slice(0, -1))
}
/**
* Replaces the value at the current path with the specified value.
* If the current context is already at the root or an empty path, it returns `treeUpdateStatus.ALREADY_AT_ROOT_OR_EMPTY_PATH`.
* If the parent is an array and the last path element is not a valid index, it returns `treeUpdateStatus.INVALID_INDEX_TYPE_FOR_ARRAY`.
* Otherwise, it replaces the value at the current path and returns `treeUpdateStatus.CONTEXT_REPLACED`.
* @param value The value to replace with.
* @returns The status of the tree update operation.
*/
public replace(
value: {} | null
):
| "CONTEXT_REPLACED"
| "ALREADY_AT_ROOT_OR_EMPTY_PATH"
| "INVALID_INDEX_TYPE_FOR_ARRAY" {
const parent = this.parent
if (!parent) {
return treeUpdateStatus.ALREADY_AT_ROOT_OR_EMPTY_PATH
}
const last = this.path[this.path.length - 1]!
if (Array.isArray(parent)) {
const res = numberSchema.safeParse(last)
if (!res.success) {
return treeUpdateStatus.INVALID_INDEX_TYPE_FOR_ARRAY
}
parent[res.data] = value
return treeUpdateStatus.CONTEXT_REPLACED
}
parent[last] = value
return treeUpdateStatus.CONTEXT_REPLACED
}
public isAtRoot(): boolean {
return this.path.length === 0
}
public isArray(): this is ArrayMutatingContext {
return Array.isArray(this.context)
}
public isRecord(): this is RecordMutatingContext {
return !this.isArray()
}
public getOrThrowRecordContext(): Record<string, any> {
if (Array.isArray(this.context)) {
throw new Error("Expected record context")
}
return this.context
}
public getOrThrowArrayContext(): any[] {
if (!Array.isArray(this.context)) {
throw new Error("Expected array context")
}
return this.context
}
}
interface RecordMutatingContext extends ObjectTraversalContext {
context: Record<string, any>
}
interface ArrayMutatingContext extends ObjectTraversalContext {
context: any[]
}
export type TreeMutatingVisitor = Visitor<ObjectTraversalContext>