-
Notifications
You must be signed in to change notification settings - Fork 640
/
json-patch.ts
145 lines (131 loc) · 3.5 KB
/
json-patch.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
import { MstError, stringStartsWith } from "../internal"
/**
* https://tools.ietf.org/html/rfc6902
* http://jsonpatch.com/
*/
export interface IJsonPatch {
readonly op: "replace" | "add" | "remove"
readonly path: string
readonly value?: any
}
export interface IReversibleJsonPatch extends IJsonPatch {
readonly oldValue: any // This goes beyond JSON-patch, but makes sure each patch can be inverse applied
}
/**
* @internal
* @hidden
*/
export function splitPatch(patch: IReversibleJsonPatch): [IJsonPatch, IJsonPatch] {
if (!("oldValue" in patch))
throw new MstError(`Patches without \`oldValue\` field cannot be inversed`)
return [stripPatch(patch), invertPatch(patch)]
}
/**
* @internal
* @hidden
*/
export function stripPatch(patch: IReversibleJsonPatch): IJsonPatch {
// strips `oldvalue` information from the patch, so that it becomes a patch conform the json-patch spec
// this removes the ability to undo the patch
switch (patch.op) {
case "add":
return { op: "add", path: patch.path, value: patch.value }
case "remove":
return { op: "remove", path: patch.path }
case "replace":
return { op: "replace", path: patch.path, value: patch.value }
}
}
function invertPatch(patch: IReversibleJsonPatch): IJsonPatch {
switch (patch.op) {
case "add":
return {
op: "remove",
path: patch.path
}
case "remove":
return {
op: "add",
path: patch.path,
value: patch.oldValue
}
case "replace":
return {
op: "replace",
path: patch.path,
value: patch.oldValue
}
}
}
/**
* Simple simple check to check it is a number.
*/
function isNumber(x: string): boolean {
return typeof x === "number"
}
/**
* Escape slashes and backslashes.
*
* http://tools.ietf.org/html/rfc6901
*/
export function escapeJsonPath(path: string): string {
if (isNumber(path) === true) {
return "" + path
}
if (path.indexOf("/") === -1 && path.indexOf("~") === -1) return path
return path.replace(/~/g, "~0").replace(/\//g, "~1")
}
/**
* Unescape slashes and backslashes.
*/
export function unescapeJsonPath(path: string): string {
return path.replace(/~1/g, "/").replace(/~0/g, "~")
}
/**
* Generates a json-path compliant json path from path parts.
*
* @param path
* @returns
*/
export function joinJsonPath(path: string[]): string {
// `/` refers to property with an empty name, while `` refers to root itself!
if (path.length === 0) return ""
const getPathStr = (p: string[]) => p.map(escapeJsonPath).join("/")
if (path[0] === "." || path[0] === "..") {
// relative
return getPathStr(path)
} else {
// absolute
return "/" + getPathStr(path)
}
}
/**
* Splits and decodes a json path into several parts.
*
* @param path
* @returns
*/
export function splitJsonPath(path: string): string[] {
// `/` refers to property with an empty name, while `` refers to root itself!
const parts = path.split("/").map(unescapeJsonPath)
const valid =
path === "" ||
path === "." ||
path === ".." ||
stringStartsWith(path, "/") ||
stringStartsWith(path, "./") ||
stringStartsWith(path, "../")
if (!valid) {
throw new MstError(`a json path must be either rooted, empty or relative, but got '${path}'`)
}
// '/a/b/c' -> ["a", "b", "c"]
// '../../b/c' -> ["..", "..", "b", "c"]
// '' -> []
// '/' -> ['']
// './a' -> [".", "a"]
// /./a' -> [".", "a"] equivalent to './a'
if (parts[0] === "") {
parts.shift()
}
return parts
}