@radio-garden/ditojs-utils 2.85.0-0.5067ad799
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 +6 -0
- package/package.json +43 -0
- package/src/__snapshots__/index.test.js.snap +88 -0
- package/src/array/flatten.js +14 -0
- package/src/array/flattten.test.js +29 -0
- package/src/array/index.js +2 -0
- package/src/array/shuffle.js +11 -0
- package/src/array/shuffle.test.js +25 -0
- package/src/base/base.js +118 -0
- package/src/base/base.test.js +590 -0
- package/src/base/index.js +1 -0
- package/src/class/index.js +1 -0
- package/src/class/mixin.js +29 -0
- package/src/class/mixin.test.js +70 -0
- package/src/dataPath/getEntriesAtDataPath.js +67 -0
- package/src/dataPath/getEntriesAtDataPath.test.js +204 -0
- package/src/dataPath/getValueAtDataPath.js +45 -0
- package/src/dataPath/getValueAtDataPath.test.js +140 -0
- package/src/dataPath/index.js +6 -0
- package/src/dataPath/normalizeDataPath.js +27 -0
- package/src/dataPath/normalizeDataPath.test.js +36 -0
- package/src/dataPath/parseDataPath.js +16 -0
- package/src/dataPath/parseDataPath.test.js +67 -0
- package/src/dataPath/setDataPathEntries.js +8 -0
- package/src/dataPath/setDataPathEntries.test.js +36 -0
- package/src/dataPath/setValueAtDataPath.js +13 -0
- package/src/dataPath/setValueAtDataPath.test.js +34 -0
- package/src/function/deprecate.js +9 -0
- package/src/function/index.js +4 -0
- package/src/function/toAsync.js +9 -0
- package/src/function/toAsync.test.js +31 -0
- package/src/function/toCallback.js +11 -0
- package/src/function/toCallback.test.js +29 -0
- package/src/function/toPromiseCallback.js +9 -0
- package/src/function/toPromiseCallback.test.js +24 -0
- package/src/html/escapeHtml.js +11 -0
- package/src/html/escapeHtml.test.js +19 -0
- package/src/html/index.js +2 -0
- package/src/html/stripHtml.js +23 -0
- package/src/html/stripHtml.test.js +37 -0
- package/src/index.js +10 -0
- package/src/index.test.js +7 -0
- package/src/object/asCallback.js +9 -0
- package/src/object/clone.js +75 -0
- package/src/object/clone.test.js +131 -0
- package/src/object/equals.js +36 -0
- package/src/object/equals.test.js +269 -0
- package/src/object/groupBy.js +15 -0
- package/src/object/groupBy.test.js +70 -0
- package/src/object/index.js +8 -0
- package/src/object/mapKeys.js +7 -0
- package/src/object/mapKeys.test.js +16 -0
- package/src/object/mapValues.js +9 -0
- package/src/object/mapValues.test.js +38 -0
- package/src/object/mergeDeeply.js +47 -0
- package/src/object/mergeDeeply.test.js +152 -0
- package/src/object/pick.js +11 -0
- package/src/object/pick.test.js +23 -0
- package/src/object/pickBy.js +11 -0
- package/src/object/pickBy.test.js +48 -0
- package/src/promise/index.js +2 -0
- package/src/promise/mapConcurrently.js +33 -0
- package/src/promise/mapSequentially.js +9 -0
- package/src/string/camelize.js +14 -0
- package/src/string/camelize.test.js +37 -0
- package/src/string/capitalize.js +7 -0
- package/src/string/capitalize.test.js +33 -0
- package/src/string/decamelize.js +27 -0
- package/src/string/decamelize.test.js +83 -0
- package/src/string/deindent.js +69 -0
- package/src/string/deindent.test.js +181 -0
- package/src/string/escapeRegexp.js +3 -0
- package/src/string/format.js +109 -0
- package/src/string/format.test.js +196 -0
- package/src/string/formatDate.js +13 -0
- package/src/string/formatDate.test.js +28 -0
- package/src/string/getCommonPrefix.js +35 -0
- package/src/string/getCommonPrefix.test.js +23 -0
- package/src/string/index.js +15 -0
- package/src/string/isAbsoluteUrl.js +7 -0
- package/src/string/isAbsoluteUrl.test.js +15 -0
- package/src/string/isCreditCard.js +21 -0
- package/src/string/isCreditCard.test.js +50 -0
- package/src/string/isDomain.js +9 -0
- package/src/string/isDomain.test.js +15 -0
- package/src/string/isEmail.js +6 -0
- package/src/string/isEmail.test.js +37 -0
- package/src/string/isHostname.js +8 -0
- package/src/string/isHostname.test.js +12 -0
- package/src/string/isUrl.js +23 -0
- package/src/string/isUrl.test.js +1595 -0
- package/src/string/labelize.js +17 -0
- package/src/string/labelize.test.js +39 -0
- package/src/timer/debounce.js +34 -0
- package/src/timer/debounce.test.js +101 -0
- package/src/timer/debounceAsync.js +60 -0
- package/src/timer/debounceAsync.test.js +143 -0
- package/src/timer/index.js +2 -0
- package/types/index.d.ts +939 -0
- package/types/tests/base.test-d.ts +172 -0
- package/types/tests/datapath.test-d.ts +75 -0
- package/types/tests/function.test-d.ts +137 -0
- package/types/tests/object.test-d.ts +190 -0
- package/types/tests/promise.test-d.ts +66 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { parseDataPath } from './parseDataPath.js'
|
|
2
|
+
import { normalizeDataPath } from './normalizeDataPath.js'
|
|
3
|
+
|
|
4
|
+
const NOT_FOUND = Symbol('NOT_FOUND')
|
|
5
|
+
|
|
6
|
+
export function getEntriesAtDataPath(
|
|
7
|
+
obj,
|
|
8
|
+
path,
|
|
9
|
+
handleError = () => {
|
|
10
|
+
throw new Error(`Invalid path: ${path}`)
|
|
11
|
+
}
|
|
12
|
+
) {
|
|
13
|
+
const parsedPath = parseDataPath(path)
|
|
14
|
+
|
|
15
|
+
const prefixEntries = (obj, index, getEntries) => {
|
|
16
|
+
const prefix = normalizeDataPath(parsedPath.slice(0, index))
|
|
17
|
+
return Object.entries(obj).reduce(
|
|
18
|
+
(map, [key, value]) => {
|
|
19
|
+
const pathKey = prefix ? `${prefix}/${key}` : key
|
|
20
|
+
for (const [path, val] of Object.entries(getEntries(value))) {
|
|
21
|
+
if (val !== NOT_FOUND) {
|
|
22
|
+
map[`${pathKey}/${path}`] = val
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return map
|
|
26
|
+
},
|
|
27
|
+
{}
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let index = 0
|
|
32
|
+
for (const part of parsedPath) {
|
|
33
|
+
if (obj && typeof obj === 'object') {
|
|
34
|
+
if (part in obj) {
|
|
35
|
+
obj = obj[part]
|
|
36
|
+
index++
|
|
37
|
+
continue
|
|
38
|
+
} else if (part === '*') {
|
|
39
|
+
// Support wildcards on arrays and objects
|
|
40
|
+
const pathNested = parsedPath.slice(index + 1)
|
|
41
|
+
return prefixEntries(
|
|
42
|
+
obj,
|
|
43
|
+
index,
|
|
44
|
+
value => getEntriesAtDataPath(value, pathNested, handleError)
|
|
45
|
+
)
|
|
46
|
+
} else if (part === '**') {
|
|
47
|
+
// Support wildcards on arrays and objects
|
|
48
|
+
const pathNested = parsedPath.slice(index + 1)
|
|
49
|
+
const pathSelf = parsedPath.slice(index)
|
|
50
|
+
return prefixEntries(
|
|
51
|
+
obj,
|
|
52
|
+
index,
|
|
53
|
+
value => ({
|
|
54
|
+
...getEntriesAtDataPath(value, pathNested, () => NOT_FOUND),
|
|
55
|
+
...getEntriesAtDataPath(value, pathSelf, () => NOT_FOUND)
|
|
56
|
+
})
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const res = handleError?.(obj, part, index)
|
|
61
|
+
// Do not add `undefined` results to the resulting entries object.
|
|
62
|
+
return res !== undefined
|
|
63
|
+
? { [normalizeDataPath(parsedPath)]: res }
|
|
64
|
+
: {}
|
|
65
|
+
}
|
|
66
|
+
return { [normalizeDataPath(parsedPath)]: obj }
|
|
67
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { getEntriesAtDataPath } from './getEntriesAtDataPath.js'
|
|
2
|
+
|
|
3
|
+
describe('getEntriesAtDataPath()', () => {
|
|
4
|
+
const data = {
|
|
5
|
+
object: {
|
|
6
|
+
array: [
|
|
7
|
+
null,
|
|
8
|
+
{
|
|
9
|
+
prop: 'expected'
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
it('should return data at a given path in property access notation', () => {
|
|
16
|
+
expect(getEntriesAtDataPath(data, 'object.array[1].prop')).toStrictEqual({
|
|
17
|
+
'object/array/1/prop': 'expected'
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('should return data at a given JSON pointer path', () => {
|
|
22
|
+
expect(getEntriesAtDataPath(data, '/object/array/1/prop')).toStrictEqual({
|
|
23
|
+
'object/array/1/prop': 'expected'
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it(`should return data at a given 'relative' JSON pointer path`, () => {
|
|
28
|
+
expect(getEntriesAtDataPath(data, 'object/array/1/prop')).toStrictEqual({
|
|
29
|
+
'object/array/1/prop': 'expected'
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should throw an error with faulty paths', () => {
|
|
34
|
+
expect(() => getEntriesAtDataPath(data, 'object/unknown/prop'))
|
|
35
|
+
.toThrow('Invalid path: object/unknown/prop')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should throw an error with nullish objects', () => {
|
|
39
|
+
expect(() => getEntriesAtDataPath(null, 'object'))
|
|
40
|
+
.toThrow('Invalid path: object')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should support custom error handler', () => {
|
|
44
|
+
const handleError = (object, part, index) => `Error: ${part}, ${index}`
|
|
45
|
+
expect(getEntriesAtDataPath(data, 'object/unknown/prop', handleError))
|
|
46
|
+
.toStrictEqual({ 'object/unknown/prop': 'Error: unknown, 1' })
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should handle non-existing paths with custom `handleError()`', () => {
|
|
50
|
+
const handleError = () => undefined
|
|
51
|
+
expect(getEntriesAtDataPath(data, 'object/unknown/prop', handleError))
|
|
52
|
+
.toStrictEqual({})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should return shallow wildcard matches', () => {
|
|
56
|
+
const data = {
|
|
57
|
+
object1: {
|
|
58
|
+
array: [
|
|
59
|
+
{
|
|
60
|
+
name: 'one',
|
|
61
|
+
array: [
|
|
62
|
+
{ name: 'one.one' },
|
|
63
|
+
{ name: 'one.two' }
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'two',
|
|
68
|
+
array: [
|
|
69
|
+
{ name: 'two.one' },
|
|
70
|
+
{ name: 'two.two' }
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
object2: {
|
|
76
|
+
object: {
|
|
77
|
+
one: {
|
|
78
|
+
name: 'one',
|
|
79
|
+
object: {
|
|
80
|
+
one: { name: 'one.one' },
|
|
81
|
+
two: { name: 'one.two' }
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
two: {
|
|
85
|
+
name: 'two',
|
|
86
|
+
object: {
|
|
87
|
+
one: { name: 'two.one' },
|
|
88
|
+
two: { name: 'two.two' }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
object3: {
|
|
94
|
+
array: [
|
|
95
|
+
{
|
|
96
|
+
one: { name: 'one.one' },
|
|
97
|
+
two: { name: 'one.two' }
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
one: { name: 'two.one' },
|
|
101
|
+
two: { name: 'two.two' }
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
expect(getEntriesAtDataPath(data, 'object1/array/*/name'))
|
|
108
|
+
.toStrictEqual({
|
|
109
|
+
'object1/array/0/name': 'one',
|
|
110
|
+
'object1/array/1/name': 'two'
|
|
111
|
+
})
|
|
112
|
+
expect(getEntriesAtDataPath(data, 'object1.array[*].name'))
|
|
113
|
+
.toStrictEqual({
|
|
114
|
+
'object1/array/0/name': 'one',
|
|
115
|
+
'object1/array/1/name': 'two'
|
|
116
|
+
})
|
|
117
|
+
expect(getEntriesAtDataPath(data, 'object1/array/*/array/*/name'))
|
|
118
|
+
.toStrictEqual({
|
|
119
|
+
'object1/array/0/array/0/name': 'one.one',
|
|
120
|
+
'object1/array/0/array/1/name': 'one.two',
|
|
121
|
+
'object1/array/1/array/0/name': 'two.one',
|
|
122
|
+
'object1/array/1/array/1/name': 'two.two'
|
|
123
|
+
})
|
|
124
|
+
expect(getEntriesAtDataPath(data, 'object2/object/*/name'))
|
|
125
|
+
.toStrictEqual({
|
|
126
|
+
'object2/object/one/name': 'one',
|
|
127
|
+
'object2/object/two/name': 'two'
|
|
128
|
+
})
|
|
129
|
+
expect(getEntriesAtDataPath(data, 'object2/object/*/object/*/name'))
|
|
130
|
+
.toStrictEqual({
|
|
131
|
+
'object2/object/one/object/one/name': 'one.one',
|
|
132
|
+
'object2/object/one/object/two/name': 'one.two',
|
|
133
|
+
'object2/object/two/object/one/name': 'two.one',
|
|
134
|
+
'object2/object/two/object/two/name': 'two.two'
|
|
135
|
+
})
|
|
136
|
+
expect(getEntriesAtDataPath(data, 'object3/array/*/*/name'))
|
|
137
|
+
.toStrictEqual({
|
|
138
|
+
'object3/array/0/one/name': 'one.one',
|
|
139
|
+
'object3/array/0/two/name': 'one.two',
|
|
140
|
+
'object3/array/1/one/name': 'two.one',
|
|
141
|
+
'object3/array/1/two/name': 'two.two'
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('should return deep wildcard matches', () => {
|
|
146
|
+
const data = {
|
|
147
|
+
object: {
|
|
148
|
+
array: [
|
|
149
|
+
{ name: 'one' },
|
|
150
|
+
{ name: 'two' },
|
|
151
|
+
{
|
|
152
|
+
object: {
|
|
153
|
+
one: { name: 'three' },
|
|
154
|
+
two: { name: 'four' }
|
|
155
|
+
},
|
|
156
|
+
array: [
|
|
157
|
+
{ name: 'five' },
|
|
158
|
+
{ name: 'six' },
|
|
159
|
+
{
|
|
160
|
+
object: {
|
|
161
|
+
one: { name: 'seven' },
|
|
162
|
+
two: { name: 'eight' }
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const expected = {
|
|
172
|
+
'object/array/0/name': 'one',
|
|
173
|
+
'object/array/1/name': 'two',
|
|
174
|
+
'object/array/2/object/one/name': 'three',
|
|
175
|
+
'object/array/2/object/two/name': 'four',
|
|
176
|
+
'object/array/2/array/0/name': 'five',
|
|
177
|
+
'object/array/2/array/1/name': 'six',
|
|
178
|
+
'object/array/2/array/2/object/one/name': 'seven',
|
|
179
|
+
'object/array/2/array/2/object/two/name': 'eight'
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let result = getEntriesAtDataPath(data, '**/name')
|
|
183
|
+
expect(result).toEqual(expected)
|
|
184
|
+
expect(Object.values(result)).toEqual(Object.values(expected))
|
|
185
|
+
|
|
186
|
+
result = getEntriesAtDataPath(data, 'object/**/name')
|
|
187
|
+
expect(result).toEqual(expected)
|
|
188
|
+
expect(Object.values(result)).toEqual(Object.values(expected))
|
|
189
|
+
|
|
190
|
+
expect(getEntriesAtDataPath(data, 'object/**/array/**/name')).toEqual({
|
|
191
|
+
'object/array/2/array/0/name': 'five',
|
|
192
|
+
'object/array/2/array/1/name': 'six',
|
|
193
|
+
'object/array/2/array/2/object/one/name': 'seven',
|
|
194
|
+
'object/array/2/array/2/object/two/name': 'eight'
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
expect(getEntriesAtDataPath(data, 'object/**/object/**/name')).toEqual({
|
|
198
|
+
'object/array/2/object/one/name': 'three',
|
|
199
|
+
'object/array/2/object/two/name': 'four',
|
|
200
|
+
'object/array/2/array/2/object/one/name': 'seven',
|
|
201
|
+
'object/array/2/array/2/object/two/name': 'eight'
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { isArray, asArray } from '../base/index.js'
|
|
2
|
+
import { parseDataPath } from './parseDataPath.js'
|
|
3
|
+
|
|
4
|
+
const NOT_FOUND = Symbol('NOT_FOUND')
|
|
5
|
+
|
|
6
|
+
export function getValueAtDataPath(
|
|
7
|
+
obj,
|
|
8
|
+
path,
|
|
9
|
+
handleError = () => {
|
|
10
|
+
throw new Error(`Invalid path: ${path}`)
|
|
11
|
+
}
|
|
12
|
+
) {
|
|
13
|
+
const parsedPath = parseDataPath(path)
|
|
14
|
+
let index = 0
|
|
15
|
+
for (const part of parsedPath) {
|
|
16
|
+
if (obj && typeof obj === 'object') {
|
|
17
|
+
if (part in obj) {
|
|
18
|
+
obj = obj[part]
|
|
19
|
+
index++
|
|
20
|
+
continue
|
|
21
|
+
} else if (part === '*') {
|
|
22
|
+
// Support wildcards on arrays and objects
|
|
23
|
+
const pathNested = parsedPath.slice(index + 1)
|
|
24
|
+
const values = isArray(obj) ? obj : Object.values(obj)
|
|
25
|
+
return values
|
|
26
|
+
.map(value => getValueAtDataPath(value, pathNested, handleError))
|
|
27
|
+
.flat(1)
|
|
28
|
+
} else if (part === '**') {
|
|
29
|
+
// Support deep wildcards on arrays and objects
|
|
30
|
+
const pathNested = parsedPath.slice(index + 1)
|
|
31
|
+
const pathSelf = parsedPath.slice(index)
|
|
32
|
+
const values = isArray(obj) ? obj : Object.values(obj)
|
|
33
|
+
return values
|
|
34
|
+
.map(value => [
|
|
35
|
+
...asArray(getValueAtDataPath(value, pathNested, () => NOT_FOUND)),
|
|
36
|
+
...asArray(getValueAtDataPath(value, pathSelf, () => NOT_FOUND))
|
|
37
|
+
])
|
|
38
|
+
.flat(1)
|
|
39
|
+
.filter(value => value !== NOT_FOUND)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return handleError?.(obj, part, index)
|
|
43
|
+
}
|
|
44
|
+
return obj
|
|
45
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { getValueAtDataPath } from './getValueAtDataPath.js'
|
|
2
|
+
|
|
3
|
+
describe('getValueAtDataPath()', () => {
|
|
4
|
+
const data = {
|
|
5
|
+
object: {
|
|
6
|
+
array: [
|
|
7
|
+
null,
|
|
8
|
+
{
|
|
9
|
+
prop: 'expected'
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
it('should return the root data for empty paths', () => {
|
|
16
|
+
expect(getValueAtDataPath(data, '')).toBe(data)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should return data at a given path in property access notation', () => {
|
|
20
|
+
expect(getValueAtDataPath(data, 'object.array[1].prop')).toBe('expected')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should return data at a given JSON pointer path', () => {
|
|
24
|
+
expect(getValueAtDataPath(data, '/object/array/1/prop')).toBe('expected')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it(`should return data at a given 'relative' JSON pointer path`, () => {
|
|
28
|
+
expect(getValueAtDataPath(data, 'object/array/1/prop')).toBe('expected')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should throw an error with faulty paths', () => {
|
|
32
|
+
expect(() => getValueAtDataPath(data, 'object/unknown/prop'))
|
|
33
|
+
.toThrow('Invalid path: object/unknown/prop')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should throw an error with nullish objects', () => {
|
|
37
|
+
expect(() => getValueAtDataPath(null, 'object'))
|
|
38
|
+
.toThrow('Invalid path: object')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should support custom error handler', () => {
|
|
42
|
+
const handleError = (object, part, index) => `Error: ${part}, ${index}`
|
|
43
|
+
expect(getValueAtDataPath(data, 'object/unknown/prop', handleError))
|
|
44
|
+
.toBe('Error: unknown, 1')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should return shallow wildcard matches', () => {
|
|
48
|
+
const data = {
|
|
49
|
+
object1: {
|
|
50
|
+
array: [
|
|
51
|
+
{ name: 'one' },
|
|
52
|
+
{ name: 'two' }
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
object2: {
|
|
56
|
+
object: {
|
|
57
|
+
one: { name: 'one' },
|
|
58
|
+
two: { name: 'two' }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
expect(getValueAtDataPath(data, 'object1/array/*/name'))
|
|
64
|
+
.toEqual(['one', 'two'])
|
|
65
|
+
expect(getValueAtDataPath(data, 'object1.array[*].name'))
|
|
66
|
+
.toEqual(['one', 'two'])
|
|
67
|
+
expect(getValueAtDataPath(data, 'object2/object/*/name'))
|
|
68
|
+
.toEqual(['one', 'two'])
|
|
69
|
+
expect(getValueAtDataPath(data, 'object2.object[*].name'))
|
|
70
|
+
.toEqual(['one', 'two'])
|
|
71
|
+
expect(getValueAtDataPath(data, '*/*/*'))
|
|
72
|
+
.toEqual([
|
|
73
|
+
{ name: 'one' },
|
|
74
|
+
{ name: 'two' },
|
|
75
|
+
{ name: 'one' },
|
|
76
|
+
{ name: 'two' }
|
|
77
|
+
])
|
|
78
|
+
expect(getValueAtDataPath(data, '*/*/*/name'))
|
|
79
|
+
.toEqual(['one', 'two', 'one', 'two'])
|
|
80
|
+
|
|
81
|
+
expect(getValueAtDataPath(data, '**/name'))
|
|
82
|
+
.toEqual(['one', 'two', 'one', 'two'])
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('should return deep wildcard matches', () => {
|
|
86
|
+
const data = {
|
|
87
|
+
object: {
|
|
88
|
+
object: {
|
|
89
|
+
array: [
|
|
90
|
+
{ name: 'one' },
|
|
91
|
+
{ name: 'two' },
|
|
92
|
+
{
|
|
93
|
+
object: {
|
|
94
|
+
one: { name: 'three' },
|
|
95
|
+
two: { name: 'four' }
|
|
96
|
+
},
|
|
97
|
+
array: [
|
|
98
|
+
{ name: 'five' },
|
|
99
|
+
{ name: 'six' },
|
|
100
|
+
{
|
|
101
|
+
object: {
|
|
102
|
+
one: { name: 'seven' },
|
|
103
|
+
two: { name: 'eight' }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
expect(getValueAtDataPath(data, 'object/**/name'))
|
|
114
|
+
.toEqual(['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight'])
|
|
115
|
+
|
|
116
|
+
expect(getValueAtDataPath(data, 'object/**/object/one/name'))
|
|
117
|
+
.toEqual(['three', 'seven'])
|
|
118
|
+
|
|
119
|
+
expect(getValueAtDataPath(data, 'object/**/object/two/name'))
|
|
120
|
+
.toEqual(['four', 'eight'])
|
|
121
|
+
|
|
122
|
+
expect(getValueAtDataPath(data, 'object/**/array/0/name'))
|
|
123
|
+
.toEqual(['one', 'five'])
|
|
124
|
+
|
|
125
|
+
expect(getValueAtDataPath(data, 'object/**/array/1/name'))
|
|
126
|
+
.toEqual(['two', 'six'])
|
|
127
|
+
|
|
128
|
+
expect(getValueAtDataPath(data, 'object/**/object'))
|
|
129
|
+
.toEqual([
|
|
130
|
+
{
|
|
131
|
+
one: { name: 'three' },
|
|
132
|
+
two: { name: 'four' }
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
one: { name: 'seven' },
|
|
136
|
+
two: { name: 'eight' }
|
|
137
|
+
}
|
|
138
|
+
])
|
|
139
|
+
})
|
|
140
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { parseDataPath } from './parseDataPath.js'
|
|
2
|
+
|
|
3
|
+
export function normalizeDataPath(path) {
|
|
4
|
+
path = parseDataPath(path)
|
|
5
|
+
// Normalize relative tokens and concatenated absolute paths.
|
|
6
|
+
for (let i = 0; i < path.length; i++) {
|
|
7
|
+
const token = path[i]
|
|
8
|
+
if (token === '.' || token === '..' && i === 0) {
|
|
9
|
+
path.splice(i, 1)
|
|
10
|
+
i--
|
|
11
|
+
} else if (token === '..') {
|
|
12
|
+
path.splice(--i, 2)
|
|
13
|
+
i--
|
|
14
|
+
} else if (token === '') {
|
|
15
|
+
if (i === path.length - 1) {
|
|
16
|
+
// Remove trailing slash
|
|
17
|
+
path.splice(i, 1)
|
|
18
|
+
} else {
|
|
19
|
+
// The beginning of a concatenated absolute path:
|
|
20
|
+
// Start from scratch, see `parseDataPath.test.js`
|
|
21
|
+
path.splice(0, i + 1)
|
|
22
|
+
i = 0
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return path.join('/')
|
|
27
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { normalizeDataPath } from './normalizeDataPath.js'
|
|
2
|
+
|
|
3
|
+
describe('normalizeDataPath()', () => {
|
|
4
|
+
it('should normalize JSON pointers', () => {
|
|
5
|
+
expect(normalizeDataPath('/object/array/1/prop'))
|
|
6
|
+
.toStrictEqual('object/array/1/prop')
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('should normalize property access notation', () => {
|
|
10
|
+
const expected = 'object/array/1/prop'
|
|
11
|
+
expect(normalizeDataPath('.object.array[1].prop')).toStrictEqual(expected)
|
|
12
|
+
expect(normalizeDataPath('.object["array"][1].prop'))
|
|
13
|
+
.toStrictEqual(expected)
|
|
14
|
+
expect(normalizeDataPath(`['object']['array'][1]['prop']`))
|
|
15
|
+
.toStrictEqual(expected)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('should normalize relative tokens', () => {
|
|
19
|
+
expect(normalizeDataPath('/object/property1/..'))
|
|
20
|
+
.toStrictEqual('object')
|
|
21
|
+
expect(normalizeDataPath('/object/property1/../'))
|
|
22
|
+
.toStrictEqual('object')
|
|
23
|
+
expect(normalizeDataPath('/object/property1/../value'))
|
|
24
|
+
.toStrictEqual('object/value')
|
|
25
|
+
expect(normalizeDataPath('/object/property1/../value/'))
|
|
26
|
+
.toStrictEqual('object/value')
|
|
27
|
+
expect(normalizeDataPath('/object/property1/../property2/../value'))
|
|
28
|
+
.toStrictEqual('object/value')
|
|
29
|
+
expect(normalizeDataPath('/object/property1/property2/../../value'))
|
|
30
|
+
.toStrictEqual('object/value')
|
|
31
|
+
expect(normalizeDataPath('/object/property1//object/value'))
|
|
32
|
+
.toStrictEqual('object/value')
|
|
33
|
+
expect(normalizeDataPath('/object1/object2/./object3/value'))
|
|
34
|
+
.toStrictEqual('object1/object2/object3/value')
|
|
35
|
+
})
|
|
36
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { isArray, isString } from '../base/index.js'
|
|
2
|
+
|
|
3
|
+
export function parseDataPath(path) {
|
|
4
|
+
if (isArray(path)) {
|
|
5
|
+
return [...path] // Alway return new arrays (clones).
|
|
6
|
+
} else if (isString(path)) {
|
|
7
|
+
if (!path) return []
|
|
8
|
+
const str = path
|
|
9
|
+
// Convert from JavaScript property access notation to JSON pointers,
|
|
10
|
+
// while preserving '..' in paths:
|
|
11
|
+
.replace(/\.([^./]+)/g, '/$1')
|
|
12
|
+
// Expand array property access notation ([])
|
|
13
|
+
.replace(/\[['"]?([^'"\]]*)['"]?\]/g, '/$1')
|
|
14
|
+
return /^\//.test(str) ? str.slice(1).split('/') : str.split('/')
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { parseDataPath } from './parseDataPath.js'
|
|
2
|
+
|
|
3
|
+
describe('parseDataPath()', () => {
|
|
4
|
+
it('should parse JSON pointers', () => {
|
|
5
|
+
expect(parseDataPath('/object/array/1/prop'))
|
|
6
|
+
.toStrictEqual(['object', 'array', '1', 'prop'])
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('should parse property access notation', () => {
|
|
10
|
+
const expected = ['object', 'array', '1', 'prop']
|
|
11
|
+
expect(parseDataPath('.object.array[1].prop')).toStrictEqual(expected)
|
|
12
|
+
expect(parseDataPath(`.object["array"][1].prop`)).toStrictEqual(expected)
|
|
13
|
+
expect(parseDataPath(`['object']['array'][1]['prop']`))
|
|
14
|
+
.toStrictEqual(expected)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it(`should parse 'relative' JSON pointers`, () => {
|
|
18
|
+
expect(parseDataPath('object/array/1/prop'))
|
|
19
|
+
.toStrictEqual(['object', 'array', '1', 'prop'])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it(`should parse 'relative' property access notation`, () => {
|
|
23
|
+
expect(parseDataPath('object.array[1].prop'))
|
|
24
|
+
.toStrictEqual(['object', 'array', '1', 'prop'])
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should parse relative and absolute tokens', () => {
|
|
28
|
+
expect(parseDataPath('/object/property1/../property2/../value'))
|
|
29
|
+
.toStrictEqual(['object', 'property1', '..', 'property2', '..', 'value'])
|
|
30
|
+
expect(parseDataPath('../object/value'))
|
|
31
|
+
.toStrictEqual(['..', 'object', 'value'])
|
|
32
|
+
expect(parseDataPath('./object/value'))
|
|
33
|
+
.toStrictEqual(['.', 'object', 'value'])
|
|
34
|
+
// This happens when concatenating a data path with another absolute one, an
|
|
35
|
+
// empty space will be interpreted as "start from scratch" when normalizing.
|
|
36
|
+
expect(parseDataPath('//object/value'))
|
|
37
|
+
.toStrictEqual(['', 'object', 'value'])
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should handle white-space in JSON pointers', () => {
|
|
41
|
+
expect(parseDataPath('/object/property name'))
|
|
42
|
+
.toStrictEqual(['object', 'property name'])
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should handle white-space in property access notation', () => {
|
|
46
|
+
const expected = ['object', 'property name']
|
|
47
|
+
expect(parseDataPath(`.object["property name"]`)).toStrictEqual(expected)
|
|
48
|
+
expect(parseDataPath(`.object['property name']`)).toStrictEqual(expected)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should return a clone if argument is already an array', () => {
|
|
52
|
+
const array = ['object', 'array', '1']
|
|
53
|
+
const actual = parseDataPath(array)
|
|
54
|
+
expect(actual).toStrictEqual(array)
|
|
55
|
+
expect(actual).not.toBe(array)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should return undefined for values other than array / string', () => {
|
|
59
|
+
expect(parseDataPath({})).toBe(undefined)
|
|
60
|
+
expect(parseDataPath(false)).toBe(undefined)
|
|
61
|
+
expect(parseDataPath(10)).toBe(undefined)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should parse an empty string to an empty array', () => {
|
|
65
|
+
expect(parseDataPath('')).toStrictEqual([])
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { setDataPathEntries } from './setDataPathEntries.js'
|
|
2
|
+
|
|
3
|
+
describe('setDataPathEntries()', () => {
|
|
4
|
+
const data = {
|
|
5
|
+
object: {
|
|
6
|
+
array: [
|
|
7
|
+
{}
|
|
8
|
+
],
|
|
9
|
+
number: 10
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const add = { prop: 'new' }
|
|
14
|
+
|
|
15
|
+
it('should add data at a path to a given object and array', () => {
|
|
16
|
+
expect(() =>
|
|
17
|
+
setDataPathEntries(data, {
|
|
18
|
+
'object.array[0].added': add,
|
|
19
|
+
'object.array[1]': add
|
|
20
|
+
})
|
|
21
|
+
)
|
|
22
|
+
.not.toThrow()
|
|
23
|
+
expect(data.object.array[0].added).toStrictEqual(add)
|
|
24
|
+
expect(data.object.array[1]).toStrictEqual(add)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should throw an error with faulty paths', () => {
|
|
28
|
+
expect(() => setDataPathEntries(data, { 'object/unknown/prop': add }))
|
|
29
|
+
.toThrow()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should throw an error with invalid target', () => {
|
|
33
|
+
expect(() => setDataPathEntries(data, { 'object/number/invalid': add }))
|
|
34
|
+
.toThrow('Invalid path: object/number/invalid')
|
|
35
|
+
})
|
|
36
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { parseDataPath } from './parseDataPath.js'
|
|
2
|
+
import { getValueAtDataPath } from './getValueAtDataPath.js'
|
|
3
|
+
|
|
4
|
+
export function setValueAtDataPath(obj, path, value) {
|
|
5
|
+
const parts = parseDataPath(path)
|
|
6
|
+
const last = parts.pop()
|
|
7
|
+
const dest = getValueAtDataPath(obj, parts)
|
|
8
|
+
if (!(dest && typeof dest === 'object')) {
|
|
9
|
+
throw new Error(`Invalid path: ${path}`)
|
|
10
|
+
}
|
|
11
|
+
dest[last] = value
|
|
12
|
+
return obj
|
|
13
|
+
}
|