@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.
Files changed (104) hide show
  1. package/README.md +6 -0
  2. package/package.json +43 -0
  3. package/src/__snapshots__/index.test.js.snap +88 -0
  4. package/src/array/flatten.js +14 -0
  5. package/src/array/flattten.test.js +29 -0
  6. package/src/array/index.js +2 -0
  7. package/src/array/shuffle.js +11 -0
  8. package/src/array/shuffle.test.js +25 -0
  9. package/src/base/base.js +118 -0
  10. package/src/base/base.test.js +590 -0
  11. package/src/base/index.js +1 -0
  12. package/src/class/index.js +1 -0
  13. package/src/class/mixin.js +29 -0
  14. package/src/class/mixin.test.js +70 -0
  15. package/src/dataPath/getEntriesAtDataPath.js +67 -0
  16. package/src/dataPath/getEntriesAtDataPath.test.js +204 -0
  17. package/src/dataPath/getValueAtDataPath.js +45 -0
  18. package/src/dataPath/getValueAtDataPath.test.js +140 -0
  19. package/src/dataPath/index.js +6 -0
  20. package/src/dataPath/normalizeDataPath.js +27 -0
  21. package/src/dataPath/normalizeDataPath.test.js +36 -0
  22. package/src/dataPath/parseDataPath.js +16 -0
  23. package/src/dataPath/parseDataPath.test.js +67 -0
  24. package/src/dataPath/setDataPathEntries.js +8 -0
  25. package/src/dataPath/setDataPathEntries.test.js +36 -0
  26. package/src/dataPath/setValueAtDataPath.js +13 -0
  27. package/src/dataPath/setValueAtDataPath.test.js +34 -0
  28. package/src/function/deprecate.js +9 -0
  29. package/src/function/index.js +4 -0
  30. package/src/function/toAsync.js +9 -0
  31. package/src/function/toAsync.test.js +31 -0
  32. package/src/function/toCallback.js +11 -0
  33. package/src/function/toCallback.test.js +29 -0
  34. package/src/function/toPromiseCallback.js +9 -0
  35. package/src/function/toPromiseCallback.test.js +24 -0
  36. package/src/html/escapeHtml.js +11 -0
  37. package/src/html/escapeHtml.test.js +19 -0
  38. package/src/html/index.js +2 -0
  39. package/src/html/stripHtml.js +23 -0
  40. package/src/html/stripHtml.test.js +37 -0
  41. package/src/index.js +10 -0
  42. package/src/index.test.js +7 -0
  43. package/src/object/asCallback.js +9 -0
  44. package/src/object/clone.js +75 -0
  45. package/src/object/clone.test.js +131 -0
  46. package/src/object/equals.js +36 -0
  47. package/src/object/equals.test.js +269 -0
  48. package/src/object/groupBy.js +15 -0
  49. package/src/object/groupBy.test.js +70 -0
  50. package/src/object/index.js +8 -0
  51. package/src/object/mapKeys.js +7 -0
  52. package/src/object/mapKeys.test.js +16 -0
  53. package/src/object/mapValues.js +9 -0
  54. package/src/object/mapValues.test.js +38 -0
  55. package/src/object/mergeDeeply.js +47 -0
  56. package/src/object/mergeDeeply.test.js +152 -0
  57. package/src/object/pick.js +11 -0
  58. package/src/object/pick.test.js +23 -0
  59. package/src/object/pickBy.js +11 -0
  60. package/src/object/pickBy.test.js +48 -0
  61. package/src/promise/index.js +2 -0
  62. package/src/promise/mapConcurrently.js +33 -0
  63. package/src/promise/mapSequentially.js +9 -0
  64. package/src/string/camelize.js +14 -0
  65. package/src/string/camelize.test.js +37 -0
  66. package/src/string/capitalize.js +7 -0
  67. package/src/string/capitalize.test.js +33 -0
  68. package/src/string/decamelize.js +27 -0
  69. package/src/string/decamelize.test.js +83 -0
  70. package/src/string/deindent.js +69 -0
  71. package/src/string/deindent.test.js +181 -0
  72. package/src/string/escapeRegexp.js +3 -0
  73. package/src/string/format.js +109 -0
  74. package/src/string/format.test.js +196 -0
  75. package/src/string/formatDate.js +13 -0
  76. package/src/string/formatDate.test.js +28 -0
  77. package/src/string/getCommonPrefix.js +35 -0
  78. package/src/string/getCommonPrefix.test.js +23 -0
  79. package/src/string/index.js +15 -0
  80. package/src/string/isAbsoluteUrl.js +7 -0
  81. package/src/string/isAbsoluteUrl.test.js +15 -0
  82. package/src/string/isCreditCard.js +21 -0
  83. package/src/string/isCreditCard.test.js +50 -0
  84. package/src/string/isDomain.js +9 -0
  85. package/src/string/isDomain.test.js +15 -0
  86. package/src/string/isEmail.js +6 -0
  87. package/src/string/isEmail.test.js +37 -0
  88. package/src/string/isHostname.js +8 -0
  89. package/src/string/isHostname.test.js +12 -0
  90. package/src/string/isUrl.js +23 -0
  91. package/src/string/isUrl.test.js +1595 -0
  92. package/src/string/labelize.js +17 -0
  93. package/src/string/labelize.test.js +39 -0
  94. package/src/timer/debounce.js +34 -0
  95. package/src/timer/debounce.test.js +101 -0
  96. package/src/timer/debounceAsync.js +60 -0
  97. package/src/timer/debounceAsync.test.js +143 -0
  98. package/src/timer/index.js +2 -0
  99. package/types/index.d.ts +939 -0
  100. package/types/tests/base.test-d.ts +172 -0
  101. package/types/tests/datapath.test-d.ts +75 -0
  102. package/types/tests/function.test-d.ts +137 -0
  103. package/types/tests/object.test-d.ts +190 -0
  104. 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,6 @@
1
+ export * from './getEntriesAtDataPath.js'
2
+ export * from './getValueAtDataPath.js'
3
+ export * from './normalizeDataPath.js'
4
+ export * from './parseDataPath.js'
5
+ export * from './setDataPathEntries.js'
6
+ export * from './setValueAtDataPath.js'
@@ -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,8 @@
1
+ import { setValueAtDataPath } from './setValueAtDataPath.js'
2
+
3
+ export function setDataPathEntries(obj, entries) {
4
+ for (const [path, value] of Object.entries(entries)) {
5
+ setValueAtDataPath(obj, path, value)
6
+ }
7
+ return obj
8
+ }
@@ -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
+ }