@scalar/json-magic 0.1.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 (185) hide show
  1. package/.turbo/turbo-build.log +9 -0
  2. package/CHANGELOG.md +7 -0
  3. package/LICENSE +21 -0
  4. package/README.md +356 -0
  5. package/dist/bundle/bundle.d.ts +292 -0
  6. package/dist/bundle/bundle.d.ts.map +1 -0
  7. package/dist/bundle/bundle.js +259 -0
  8. package/dist/bundle/bundle.js.map +7 -0
  9. package/dist/bundle/create-limiter.d.ts +21 -0
  10. package/dist/bundle/create-limiter.d.ts.map +1 -0
  11. package/dist/bundle/create-limiter.js +31 -0
  12. package/dist/bundle/create-limiter.js.map +7 -0
  13. package/dist/bundle/index.d.ts +2 -0
  14. package/dist/bundle/index.d.ts.map +1 -0
  15. package/dist/bundle/index.js +5 -0
  16. package/dist/bundle/index.js.map +7 -0
  17. package/dist/bundle/plugins/browser.d.ts +4 -0
  18. package/dist/bundle/plugins/browser.d.ts.map +1 -0
  19. package/dist/bundle/plugins/browser.js +9 -0
  20. package/dist/bundle/plugins/browser.js.map +7 -0
  21. package/dist/bundle/plugins/fetch-urls/index.d.ts +39 -0
  22. package/dist/bundle/plugins/fetch-urls/index.d.ts.map +1 -0
  23. package/dist/bundle/plugins/fetch-urls/index.js +48 -0
  24. package/dist/bundle/plugins/fetch-urls/index.js.map +7 -0
  25. package/dist/bundle/plugins/node.d.ts +5 -0
  26. package/dist/bundle/plugins/node.d.ts.map +1 -0
  27. package/dist/bundle/plugins/node.js +11 -0
  28. package/dist/bundle/plugins/node.js.map +7 -0
  29. package/dist/bundle/plugins/parse-json/index.d.ts +13 -0
  30. package/dist/bundle/plugins/parse-json/index.d.ts.map +1 -0
  31. package/dist/bundle/plugins/parse-json/index.js +22 -0
  32. package/dist/bundle/plugins/parse-json/index.js.map +7 -0
  33. package/dist/bundle/plugins/parse-yaml/index.d.ts +13 -0
  34. package/dist/bundle/plugins/parse-yaml/index.d.ts.map +1 -0
  35. package/dist/bundle/plugins/parse-yaml/index.js +23 -0
  36. package/dist/bundle/plugins/parse-yaml/index.js.map +7 -0
  37. package/dist/bundle/plugins/read-files/index.d.ts +29 -0
  38. package/dist/bundle/plugins/read-files/index.d.ts.map +1 -0
  39. package/dist/bundle/plugins/read-files/index.js +30 -0
  40. package/dist/bundle/plugins/read-files/index.js.map +7 -0
  41. package/dist/bundle/value-generator.d.ts +79 -0
  42. package/dist/bundle/value-generator.d.ts.map +1 -0
  43. package/dist/bundle/value-generator.js +55 -0
  44. package/dist/bundle/value-generator.js.map +7 -0
  45. package/dist/dereference/dereference.d.ts +45 -0
  46. package/dist/dereference/dereference.d.ts.map +1 -0
  47. package/dist/dereference/dereference.js +37 -0
  48. package/dist/dereference/dereference.js.map +7 -0
  49. package/dist/dereference/index.d.ts +2 -0
  50. package/dist/dereference/index.d.ts.map +1 -0
  51. package/dist/dereference/index.js +5 -0
  52. package/dist/dereference/index.js.map +7 -0
  53. package/dist/diff/apply.d.ts +35 -0
  54. package/dist/diff/apply.d.ts.map +1 -0
  55. package/dist/diff/apply.js +40 -0
  56. package/dist/diff/apply.js.map +7 -0
  57. package/dist/diff/diff.d.ts +56 -0
  58. package/dist/diff/diff.d.ts.map +1 -0
  59. package/dist/diff/diff.js +33 -0
  60. package/dist/diff/diff.js.map +7 -0
  61. package/dist/diff/index.d.ts +5 -0
  62. package/dist/diff/index.d.ts.map +1 -0
  63. package/dist/diff/index.js +9 -0
  64. package/dist/diff/index.js.map +7 -0
  65. package/dist/diff/merge.d.ts +43 -0
  66. package/dist/diff/merge.d.ts.map +1 -0
  67. package/dist/diff/merge.js +61 -0
  68. package/dist/diff/merge.js.map +7 -0
  69. package/dist/diff/trie.d.ts +64 -0
  70. package/dist/diff/trie.d.ts.map +1 -0
  71. package/dist/diff/trie.js +82 -0
  72. package/dist/diff/trie.js.map +7 -0
  73. package/dist/diff/utils.d.ts +63 -0
  74. package/dist/diff/utils.d.ts.map +1 -0
  75. package/dist/diff/utils.js +48 -0
  76. package/dist/diff/utils.js.map +7 -0
  77. package/dist/magic-proxy/index.d.ts +2 -0
  78. package/dist/magic-proxy/index.d.ts.map +1 -0
  79. package/dist/magic-proxy/index.js +6 -0
  80. package/dist/magic-proxy/index.js.map +7 -0
  81. package/dist/magic-proxy/proxy.d.ts +63 -0
  82. package/dist/magic-proxy/proxy.d.ts.map +1 -0
  83. package/dist/magic-proxy/proxy.js +108 -0
  84. package/dist/magic-proxy/proxy.js.map +7 -0
  85. package/dist/polyfills/index.d.ts +2 -0
  86. package/dist/polyfills/index.d.ts.map +1 -0
  87. package/dist/polyfills/index.js +25 -0
  88. package/dist/polyfills/index.js.map +7 -0
  89. package/dist/polyfills/path.d.ts +24 -0
  90. package/dist/polyfills/path.d.ts.map +1 -0
  91. package/dist/polyfills/path.js +174 -0
  92. package/dist/polyfills/path.js.map +7 -0
  93. package/dist/types.d.ts +2 -0
  94. package/dist/types.d.ts.map +1 -0
  95. package/dist/types.js +1 -0
  96. package/dist/types.js.map +7 -0
  97. package/dist/utils/escape-json-pointer.d.ts +7 -0
  98. package/dist/utils/escape-json-pointer.d.ts.map +1 -0
  99. package/dist/utils/escape-json-pointer.js +7 -0
  100. package/dist/utils/escape-json-pointer.js.map +7 -0
  101. package/dist/utils/get-segments-from-path.d.ts +5 -0
  102. package/dist/utils/get-segments-from-path.d.ts.map +1 -0
  103. package/dist/utils/get-segments-from-path.js +11 -0
  104. package/dist/utils/get-segments-from-path.js.map +7 -0
  105. package/dist/utils/is-json-object.d.ts +18 -0
  106. package/dist/utils/is-json-object.d.ts.map +1 -0
  107. package/dist/utils/is-json-object.js +16 -0
  108. package/dist/utils/is-json-object.js.map +7 -0
  109. package/dist/utils/is-object.d.ts +5 -0
  110. package/dist/utils/is-object.d.ts.map +1 -0
  111. package/dist/utils/is-object.js +5 -0
  112. package/dist/utils/is-object.js.map +7 -0
  113. package/dist/utils/is-yaml.d.ts +17 -0
  114. package/dist/utils/is-yaml.d.ts.map +1 -0
  115. package/dist/utils/is-yaml.js +7 -0
  116. package/dist/utils/is-yaml.js.map +7 -0
  117. package/dist/utils/json-path-utils.d.ts +23 -0
  118. package/dist/utils/json-path-utils.d.ts.map +1 -0
  119. package/dist/utils/json-path-utils.js +16 -0
  120. package/dist/utils/json-path-utils.js.map +7 -0
  121. package/dist/utils/normalize.d.ts +5 -0
  122. package/dist/utils/normalize.d.ts.map +1 -0
  123. package/dist/utils/normalize.js +28 -0
  124. package/dist/utils/normalize.js.map +7 -0
  125. package/dist/utils/unescape-json-pointer.d.ts +8 -0
  126. package/dist/utils/unescape-json-pointer.d.ts.map +1 -0
  127. package/dist/utils/unescape-json-pointer.js +7 -0
  128. package/dist/utils/unescape-json-pointer.js.map +7 -0
  129. package/esbuild.ts +13 -0
  130. package/package.json +65 -0
  131. package/src/bundle/bundle.test.ts +1843 -0
  132. package/src/bundle/bundle.ts +758 -0
  133. package/src/bundle/create-limiter.test.ts +28 -0
  134. package/src/bundle/create-limiter.ts +52 -0
  135. package/src/bundle/index.ts +2 -0
  136. package/src/bundle/plugins/browser.ts +4 -0
  137. package/src/bundle/plugins/fetch-urls/index.test.ts +147 -0
  138. package/src/bundle/plugins/fetch-urls/index.ts +94 -0
  139. package/src/bundle/plugins/node.ts +5 -0
  140. package/src/bundle/plugins/parse-json/index.test.ts +22 -0
  141. package/src/bundle/plugins/parse-json/index.ts +30 -0
  142. package/src/bundle/plugins/parse-yaml/index.test.ts +24 -0
  143. package/src/bundle/plugins/parse-yaml/index.ts +31 -0
  144. package/src/bundle/plugins/read-files/index.test.ts +35 -0
  145. package/src/bundle/plugins/read-files/index.ts +55 -0
  146. package/src/bundle/value-generator.test.ts +166 -0
  147. package/src/bundle/value-generator.ts +147 -0
  148. package/src/dereference/dereference.test.ts +137 -0
  149. package/src/dereference/dereference.ts +84 -0
  150. package/src/dereference/index.ts +2 -0
  151. package/src/diff/apply.test.ts +262 -0
  152. package/src/diff/apply.ts +78 -0
  153. package/src/diff/diff.test.ts +328 -0
  154. package/src/diff/diff.ts +94 -0
  155. package/src/diff/index.test.ts +150 -0
  156. package/src/diff/index.ts +5 -0
  157. package/src/diff/merge.test.ts +1109 -0
  158. package/src/diff/merge.ts +136 -0
  159. package/src/diff/trie.test.ts +30 -0
  160. package/src/diff/trie.ts +113 -0
  161. package/src/diff/utils.test.ts +169 -0
  162. package/src/diff/utils.ts +113 -0
  163. package/src/magic-proxy/index.ts +2 -0
  164. package/src/magic-proxy/proxy.test.ts +145 -0
  165. package/src/magic-proxy/proxy.ts +225 -0
  166. package/src/polyfills/index.ts +12 -0
  167. package/src/polyfills/path.ts +248 -0
  168. package/src/types.ts +1 -0
  169. package/src/utils/escape-json-pointer.test.ts +13 -0
  170. package/src/utils/escape-json-pointer.ts +8 -0
  171. package/src/utils/get-segments-from-path.test.ts +17 -0
  172. package/src/utils/get-segments-from-path.ts +17 -0
  173. package/src/utils/is-json-object.ts +31 -0
  174. package/src/utils/is-object.test.ts +27 -0
  175. package/src/utils/is-object.ts +4 -0
  176. package/src/utils/is-yaml.ts +18 -0
  177. package/src/utils/json-path-utils.test.ts +13 -0
  178. package/src/utils/json-path-utils.ts +38 -0
  179. package/src/utils/normalize.test.ts +91 -0
  180. package/src/utils/normalize.ts +34 -0
  181. package/src/utils/unescape-json-pointer.test.ts +23 -0
  182. package/src/utils/unescape-json-pointer.ts +9 -0
  183. package/tsconfig.build.json +12 -0
  184. package/tsconfig.json +16 -0
  185. package/vite.config.ts +8 -0
@@ -0,0 +1,1109 @@
1
+ import { merge } from '@/diff/merge'
2
+ import { diff } from '@/diff/diff'
3
+ import { describe, expect, test } from 'vitest'
4
+
5
+ const deepClone = <T extends object>(obj: T) => JSON.parse(JSON.stringify(obj)) as T
6
+
7
+ type DeepPartial<T> = T extends object
8
+ ? {
9
+ [P in keyof T]?: DeepPartial<T[P]>
10
+ }
11
+ : T
12
+
13
+ describe('mergeDiff', () => {
14
+ test('should correctly merge the diffs and get conflicting changes', () => {
15
+ const baseDoc = {
16
+ openapi: '3.0.0',
17
+ info: {
18
+ title: 'Simple API',
19
+ description: 'A small OpenAPI specification example',
20
+ version: '1.0.0',
21
+ },
22
+ }
23
+
24
+ const doc1 = {
25
+ ...baseDoc,
26
+ info: {
27
+ ...baseDoc.info,
28
+ title: 'New title',
29
+ },
30
+ }
31
+
32
+ const clonedBase = deepClone<Partial<typeof baseDoc>>(baseDoc)
33
+ const deletedInformation = clonedBase.info
34
+ delete clonedBase.info
35
+
36
+ const doc2 = {
37
+ ...clonedBase,
38
+ }
39
+
40
+ expect(merge(diff(baseDoc, doc1), diff(baseDoc, doc2))).toEqual({
41
+ diffs: [],
42
+ conflicts: [
43
+ [
44
+ [
45
+ {
46
+ path: ['info', 'title'],
47
+ changes: doc1.info.title,
48
+ type: 'update',
49
+ },
50
+ ],
51
+ [{ path: ['info'], changes: deletedInformation, type: 'delete' }],
52
+ ],
53
+ ],
54
+ })
55
+
56
+ expect(merge(diff(baseDoc, doc2), diff(baseDoc, doc1))).toEqual({
57
+ diffs: [],
58
+ conflicts: [
59
+ [
60
+ [{ path: ['info'], changes: deletedInformation, type: 'delete' }],
61
+ [
62
+ {
63
+ path: ['info', 'title'],
64
+ changes: doc1.info.title,
65
+ type: 'update',
66
+ },
67
+ ],
68
+ ],
69
+ ],
70
+ })
71
+ })
72
+
73
+ test('should correctly merge the diffs and get deeply nested conflicting changes', () => {
74
+ const baseDoc = {
75
+ openapi: '3.0.0',
76
+ info: {
77
+ title: 'Simple API',
78
+ description: 'A small OpenAPI specification example',
79
+ version: '1.0.0',
80
+ },
81
+ paths: {
82
+ '/users': {
83
+ get: {
84
+ summary: 'Get a list of users',
85
+ operationId: 'getUsers',
86
+ responses: {
87
+ '200': {
88
+ description: 'A list of users',
89
+ content: {
90
+ 'application/json': {
91
+ schema: {
92
+ type: 'array',
93
+ items: {
94
+ type: 'object',
95
+ properties: {
96
+ id: { type: 'integer' },
97
+ name: { type: 'string' },
98
+ },
99
+ },
100
+ },
101
+ },
102
+ },
103
+ },
104
+ },
105
+ },
106
+ },
107
+ },
108
+ }
109
+
110
+ const doc1 = {
111
+ ...baseDoc,
112
+ paths: {
113
+ ...baseDoc.paths,
114
+ '/users': {
115
+ ...baseDoc.paths?.['/users'],
116
+ delete: {
117
+ summary: 'Delete all users',
118
+ operationId: 'delete',
119
+ responses: {
120
+ '200': {
121
+ description: 'All users deleted successfully',
122
+ },
123
+ },
124
+ },
125
+ },
126
+ '/users/{id}': {
127
+ get: {
128
+ summary: 'Get a user by ID',
129
+ operationId: 'getUserById',
130
+ parameters: [
131
+ {
132
+ name: 'id',
133
+ in: 'path',
134
+ required: true,
135
+ schema: { type: 'integer' },
136
+ },
137
+ ],
138
+ responses: {
139
+ '200': {
140
+ description: 'User details',
141
+ content: {
142
+ 'application/json': {
143
+ schema: {
144
+ type: 'object',
145
+ properties: {
146
+ id: { type: 'integer' },
147
+ name: { type: 'string' },
148
+ },
149
+ },
150
+ },
151
+ },
152
+ },
153
+ '404': {
154
+ description: 'User not found',
155
+ },
156
+ },
157
+ },
158
+ },
159
+ },
160
+ }
161
+
162
+ const clonedBase = deepClone<DeepPartial<typeof baseDoc>>(baseDoc)
163
+ const deletedDescription = clonedBase.info?.description
164
+ delete clonedBase.info?.description
165
+
166
+ const doc2 = {
167
+ ...clonedBase,
168
+ paths: {
169
+ ...clonedBase.paths,
170
+ '/users': {
171
+ ...clonedBase.paths?.['/users'],
172
+ delete: {
173
+ summary: 'Delete all users',
174
+ operationId: 'deleteUsers',
175
+ responses: {
176
+ '200': {
177
+ description: 'All users deleted successfully',
178
+ },
179
+ },
180
+ },
181
+ },
182
+ },
183
+ }
184
+
185
+ expect(merge(diff(baseDoc, doc1), diff(baseDoc, doc2))).toEqual({
186
+ diffs: [
187
+ {
188
+ path: ['paths', '/users/{id}'],
189
+ changes: doc1.paths['/users/{id}'],
190
+ type: 'add',
191
+ },
192
+ {
193
+ path: ['info', 'description'],
194
+ changes: deletedDescription,
195
+ type: 'delete',
196
+ },
197
+ ],
198
+ conflicts: [
199
+ [
200
+ [
201
+ {
202
+ path: ['paths', '/users', 'delete'],
203
+ changes: doc1.paths['/users'].delete,
204
+ type: 'add',
205
+ },
206
+ ],
207
+ [
208
+ {
209
+ path: ['paths', '/users', 'delete'],
210
+ changes: doc2.paths['/users'].delete,
211
+ type: 'add',
212
+ },
213
+ ],
214
+ ],
215
+ ],
216
+ })
217
+
218
+ expect(merge(diff(baseDoc, doc2), diff(baseDoc, doc1))).toEqual({
219
+ diffs: [
220
+ {
221
+ path: ['info', 'description'],
222
+ changes: deletedDescription,
223
+ type: 'delete',
224
+ },
225
+ {
226
+ path: ['paths', '/users/{id}'],
227
+ changes: doc1.paths['/users/{id}'],
228
+ type: 'add',
229
+ },
230
+ ],
231
+ conflicts: [
232
+ [
233
+ [
234
+ {
235
+ path: ['paths', '/users', 'delete'],
236
+ changes: doc2.paths['/users'].delete,
237
+ type: 'add',
238
+ },
239
+ ],
240
+ [
241
+ {
242
+ path: ['paths', '/users', 'delete'],
243
+ changes: doc1.paths['/users'].delete,
244
+ type: 'add',
245
+ },
246
+ ],
247
+ ],
248
+ ],
249
+ })
250
+ })
251
+
252
+ test('non conflicting addition of paths', () => {
253
+ const base = {
254
+ openapi: '3.0.0',
255
+ info: {
256
+ title: 'Sample API',
257
+ version: '1.0',
258
+ },
259
+ paths: {
260
+ '/users': {
261
+ get: {
262
+ summary: 'Get users',
263
+ responses: {
264
+ 200: {
265
+ description: 'Successful response',
266
+ },
267
+ },
268
+ },
269
+ },
270
+ },
271
+ }
272
+
273
+ const doc1 = {
274
+ ...base,
275
+ paths: {
276
+ ...base.paths,
277
+ '/products': {
278
+ get: {
279
+ summary: 'Get products',
280
+ responses: {
281
+ 200: {
282
+ description: 'Successful response',
283
+ },
284
+ },
285
+ },
286
+ },
287
+ },
288
+ }
289
+
290
+ const doc2 = {
291
+ ...base,
292
+ paths: {
293
+ ...base.paths,
294
+ '/orders': {
295
+ get: {
296
+ summary: 'Get orders',
297
+ responses: {
298
+ 200: {
299
+ description: 'Successful response',
300
+ },
301
+ },
302
+ },
303
+ },
304
+ },
305
+ }
306
+
307
+ expect(merge(diff(base, doc1), diff(base, doc2))).toEqual({
308
+ diffs: [
309
+ {
310
+ path: ['paths', '/products'],
311
+ changes: doc1.paths['/products'],
312
+ type: 'add',
313
+ },
314
+ {
315
+ path: ['paths', '/orders'],
316
+ changes: doc2.paths['/orders'],
317
+ type: 'add',
318
+ },
319
+ ],
320
+ conflicts: [],
321
+ })
322
+
323
+ expect(merge(diff(base, doc2), diff(base, doc1))).toEqual({
324
+ diffs: [
325
+ {
326
+ path: ['paths', '/orders'],
327
+ changes: doc2.paths['/orders'],
328
+ type: 'add',
329
+ },
330
+ {
331
+ path: ['paths', '/products'],
332
+ changes: doc1.paths['/products'],
333
+ type: 'add',
334
+ },
335
+ ],
336
+ conflicts: [],
337
+ })
338
+ })
339
+
340
+ test('updates in the same path', () => {
341
+ const base = {
342
+ openapi: '3.0.0',
343
+ info: {
344
+ title: 'Sample API',
345
+ version: '1.0',
346
+ },
347
+ paths: {
348
+ '/users': {
349
+ get: {
350
+ summary: 'Get users',
351
+ responses: {
352
+ 200: {
353
+ description: 'Successful response',
354
+ },
355
+ },
356
+ },
357
+ },
358
+ },
359
+ }
360
+
361
+ const doc1 = {
362
+ ...base,
363
+ paths: {
364
+ ...base.paths,
365
+ '/users': {
366
+ get: {
367
+ summary: 'Retrieve all users',
368
+ responses: {
369
+ 200: {
370
+ description: 'Successful response',
371
+ },
372
+ },
373
+ },
374
+ },
375
+ },
376
+ }
377
+
378
+ const doc2 = {
379
+ ...base,
380
+ paths: {
381
+ ...base.paths,
382
+ '/users': {
383
+ get: {
384
+ summary: 'List all users',
385
+ responses: {
386
+ 200: {
387
+ description: 'Successful response',
388
+ },
389
+ },
390
+ },
391
+ },
392
+ },
393
+ }
394
+
395
+ expect(merge(diff(base, doc1), diff(base, doc2))).toEqual({
396
+ diffs: [],
397
+ conflicts: [
398
+ [
399
+ [
400
+ {
401
+ path: ['paths', '/users', 'get', 'summary'],
402
+ changes: doc1.paths['/users'].get.summary,
403
+ type: 'update',
404
+ },
405
+ ],
406
+ [
407
+ {
408
+ path: ['paths', '/users', 'get', 'summary'],
409
+ changes: doc2.paths['/users'].get.summary,
410
+ type: 'update',
411
+ },
412
+ ],
413
+ ],
414
+ ],
415
+ })
416
+
417
+ expect(merge(diff(base, doc2), diff(base, doc1))).toEqual({
418
+ diffs: [],
419
+ conflicts: [
420
+ [
421
+ [
422
+ {
423
+ path: ['paths', '/users', 'get', 'summary'],
424
+ changes: doc2.paths['/users'].get.summary,
425
+ type: 'update',
426
+ },
427
+ ],
428
+ [
429
+ {
430
+ path: ['paths', '/users', 'get', 'summary'],
431
+ changes: doc1.paths['/users'].get.summary,
432
+ type: 'update',
433
+ },
434
+ ],
435
+ ],
436
+ ],
437
+ })
438
+ })
439
+
440
+ test('removing and modifying the same path', () => {
441
+ const base = {
442
+ openapi: '3.0.0',
443
+ info: {
444
+ title: 'Sample API',
445
+ version: '1.0',
446
+ },
447
+ paths: {
448
+ '/users': {
449
+ get: {
450
+ summary: 'Get users',
451
+ responses: {
452
+ 200: {
453
+ description: 'Successful response',
454
+ },
455
+ },
456
+ },
457
+ },
458
+ },
459
+ }
460
+
461
+ const baseClone = deepClone<DeepPartial<typeof base>>(base)
462
+ const removePaths = baseClone.paths?.['/users']?.get?.summary
463
+ delete baseClone.paths?.['/users']?.get?.summary
464
+
465
+ const doc1 = {
466
+ ...baseClone,
467
+ }
468
+
469
+ const doc2 = {
470
+ ...base,
471
+ paths: {
472
+ ...base.paths,
473
+ '/users': {
474
+ ...base.paths['/users'],
475
+ get: {
476
+ ...base.paths['/users'].get,
477
+ summary: 'Get all registered users',
478
+ },
479
+ },
480
+ },
481
+ }
482
+
483
+ expect(merge(diff(base, doc1), diff(base, doc2))).toEqual({
484
+ diffs: [],
485
+ conflicts: [
486
+ [
487
+ [
488
+ {
489
+ path: ['paths', '/users', 'get', 'summary'],
490
+ changes: removePaths,
491
+ type: 'delete',
492
+ },
493
+ ],
494
+ [
495
+ {
496
+ path: ['paths', '/users', 'get', 'summary'],
497
+ changes: doc2.paths['/users'].get.summary,
498
+ type: 'update',
499
+ },
500
+ ],
501
+ ],
502
+ ],
503
+ })
504
+
505
+ expect(merge(diff(base, doc2), diff(base, doc1))).toEqual({
506
+ diffs: [],
507
+ conflicts: [
508
+ [
509
+ [
510
+ {
511
+ path: ['paths', '/users', 'get', 'summary'],
512
+ changes: doc2.paths['/users'].get.summary,
513
+ type: 'update',
514
+ },
515
+ ],
516
+ [
517
+ {
518
+ path: ['paths', '/users', 'get', 'summary'],
519
+ changes: removePaths,
520
+ type: 'delete',
521
+ },
522
+ ],
523
+ ],
524
+ ],
525
+ })
526
+ })
527
+
528
+ test('delete parent node and inner children node should only keep the higher level change', () => {
529
+ const base = {
530
+ openapi: '3.0.0',
531
+ info: {
532
+ title: 'Sample API',
533
+ version: '1.0',
534
+ },
535
+ paths: {
536
+ '/users': {
537
+ get: {
538
+ summary: 'Get users',
539
+ responses: {
540
+ 200: {
541
+ description: 'Successful response',
542
+ },
543
+ },
544
+ },
545
+ },
546
+ },
547
+ }
548
+
549
+ const doc1 = deepClone<DeepPartial<typeof base>>(base)
550
+ delete doc1.paths?.['/users']?.get?.summary
551
+
552
+ const doc2 = deepClone<DeepPartial<typeof base>>(base)
553
+ const removePaths2 = doc2.paths?.['/users']
554
+ delete doc2.paths?.['/users']
555
+
556
+ expect(merge(diff(base, doc1), diff(base, doc2))).toEqual({
557
+ diffs: [
558
+ {
559
+ path: ['paths', '/users'],
560
+ changes: removePaths2,
561
+ type: 'delete',
562
+ },
563
+ ],
564
+ conflicts: [],
565
+ })
566
+
567
+ expect(merge(diff(base, doc2), diff(base, doc1))).toEqual({
568
+ diffs: [
569
+ {
570
+ path: ['paths', '/users'],
571
+ changes: removePaths2,
572
+ type: 'delete',
573
+ },
574
+ ],
575
+ conflicts: [],
576
+ })
577
+ })
578
+
579
+ test('deleting the same path on both documents should not resolve in a conflict', () => {
580
+ const base = {
581
+ openapi: '3.0.0',
582
+ info: {
583
+ title: 'Sample API',
584
+ version: '1.0',
585
+ },
586
+ paths: {
587
+ '/users': {
588
+ get: {
589
+ summary: 'Get users',
590
+ responses: {
591
+ 200: {
592
+ description: 'Successful response',
593
+ },
594
+ },
595
+ },
596
+ },
597
+ },
598
+ }
599
+
600
+ const doc1 = deepClone<DeepPartial<typeof base>>(base)
601
+ const doc2 = deepClone<DeepPartial<typeof base>>(base)
602
+
603
+ delete doc1.paths?.['/users']?.get
604
+ delete doc2.paths?.['/users']?.get
605
+
606
+ expect(merge(diff(base, doc1), diff(base, doc2))).toEqual({
607
+ diffs: [
608
+ {
609
+ type: 'delete',
610
+ changes: base.paths['/users'].get,
611
+ path: ['paths', '/users', 'get'],
612
+ },
613
+ ],
614
+ conflicts: [],
615
+ })
616
+
617
+ expect(merge(diff(base, doc2), diff(base, doc1))).toEqual({
618
+ diffs: [
619
+ {
620
+ type: 'delete',
621
+ changes: base.paths['/users'].get,
622
+ path: ['paths', '/users', 'get'],
623
+ },
624
+ ],
625
+ conflicts: [],
626
+ })
627
+ })
628
+
629
+ test('adding the same key should not resolve in a conflict', () => {
630
+ const base = {
631
+ openapi: '3.0.0',
632
+ info: {
633
+ title: 'Sample API',
634
+ version: '1.0',
635
+ },
636
+ }
637
+
638
+ const doc1 = {
639
+ ...base,
640
+ info: {
641
+ ...base.info,
642
+ description: 'Provides a way to interact with the playground',
643
+ },
644
+ }
645
+
646
+ const doc2 = {
647
+ ...base,
648
+ info: {
649
+ ...base.info,
650
+ description: doc1.info.description,
651
+ },
652
+ }
653
+
654
+ expect(merge(diff(base, doc1), diff(base, doc2))).toEqual({
655
+ diffs: [
656
+ {
657
+ type: 'add',
658
+ changes: doc1.info.description,
659
+ path: ['info', 'description'],
660
+ },
661
+ ],
662
+ conflicts: [],
663
+ })
664
+
665
+ expect(merge(diff(base, doc2), diff(base, doc1))).toEqual({
666
+ diffs: [
667
+ {
668
+ type: 'add',
669
+ changes: doc1.info.description,
670
+ path: ['info', 'description'],
671
+ },
672
+ ],
673
+ conflicts: [],
674
+ })
675
+ })
676
+
677
+ test('adding the same object should not resolve in a conflict', () => {
678
+ const base = {
679
+ openapi: '3.0.0',
680
+ }
681
+
682
+ const doc1 = {
683
+ ...base,
684
+ info: {
685
+ title: 'Sample API',
686
+ version: '1.0',
687
+ },
688
+ }
689
+
690
+ const doc2 = {
691
+ ...base,
692
+ info: {
693
+ title: 'Sample API',
694
+ version: '1.0',
695
+ },
696
+ }
697
+
698
+ expect(merge(diff(base, doc1), diff(base, doc2))).toEqual({
699
+ diffs: [
700
+ {
701
+ type: 'add',
702
+ changes: doc1.info,
703
+ path: ['info'],
704
+ },
705
+ ],
706
+ conflicts: [],
707
+ })
708
+
709
+ expect(merge(diff(base, doc2), diff(base, doc1))).toEqual({
710
+ diffs: [
711
+ {
712
+ type: 'add',
713
+ changes: doc1.info,
714
+ path: ['info'],
715
+ },
716
+ ],
717
+ conflicts: [],
718
+ })
719
+ })
720
+
721
+ test("adding similar object that can't be merged in the same path should result in a conflict", () => {
722
+ const base = {
723
+ openapi: '3.0.0',
724
+ }
725
+
726
+ const doc1 = {
727
+ ...base,
728
+ info: {
729
+ title: 'Sample API',
730
+ version: '1.0',
731
+ },
732
+ }
733
+
734
+ const doc2 = {
735
+ ...base,
736
+ info: {
737
+ title: 'Sample',
738
+ version: '1.0',
739
+ },
740
+ }
741
+
742
+ expect(merge(diff(base, doc1), diff(base, doc2))).toEqual({
743
+ diffs: [],
744
+ conflicts: [
745
+ [
746
+ [
747
+ {
748
+ type: 'add',
749
+ changes: doc1.info,
750
+ path: ['info'],
751
+ },
752
+ ],
753
+ [
754
+ {
755
+ type: 'add',
756
+ changes: doc2.info,
757
+ path: ['info'],
758
+ },
759
+ ],
760
+ ],
761
+ ],
762
+ })
763
+
764
+ expect(merge(diff(base, doc2), diff(base, doc1))).toEqual({
765
+ diffs: [],
766
+ conflicts: [
767
+ [
768
+ [
769
+ {
770
+ type: 'add',
771
+ changes: doc2.info,
772
+ path: ['info'],
773
+ },
774
+ ],
775
+ [
776
+ {
777
+ type: 'add',
778
+ changes: doc1.info,
779
+ path: ['info'],
780
+ },
781
+ ],
782
+ ],
783
+ ],
784
+ })
785
+ })
786
+
787
+ test('adding the same value on the same path should not result in a conflict', () => {
788
+ const base = {
789
+ openapi: '3.0.0',
790
+ info: {
791
+ title: 'Sample API',
792
+ },
793
+ }
794
+
795
+ const doc1 = {
796
+ ...base,
797
+ info: {
798
+ ...base.info,
799
+ version: '1.0',
800
+ },
801
+ }
802
+
803
+ const doc2 = {
804
+ ...base,
805
+ info: {
806
+ ...base.info,
807
+ version: '1.0',
808
+ },
809
+ }
810
+
811
+ expect(merge(diff(base, doc1), diff(base, doc2))).toEqual({
812
+ diffs: [
813
+ {
814
+ type: 'add',
815
+ changes: doc1.info.version,
816
+ path: ['info', 'version'],
817
+ },
818
+ ],
819
+ conflicts: [],
820
+ })
821
+
822
+ expect(merge(diff(base, doc2), diff(base, doc1))).toEqual({
823
+ diffs: [
824
+ {
825
+ type: 'add',
826
+ changes: doc1.info.version,
827
+ path: ['info', 'version'],
828
+ },
829
+ ],
830
+ conflicts: [],
831
+ })
832
+ })
833
+
834
+ test('should return multiple one to many conflicts for the related conflicts', () => {
835
+ const base = {
836
+ openapi: '3.0.0',
837
+ info: {
838
+ title: 'Sample API',
839
+ version: '1.0',
840
+ },
841
+ paths: {
842
+ '/users': {
843
+ get: {
844
+ summary: 'Get users',
845
+ responses: {
846
+ 200: {
847
+ description: 'Successful response',
848
+ },
849
+ },
850
+ },
851
+ },
852
+ '/pets': {
853
+ get: {
854
+ summary: 'Get pets',
855
+ responses: {
856
+ 200: {
857
+ description: 'Successful response',
858
+ },
859
+ },
860
+ },
861
+ },
862
+ },
863
+ }
864
+
865
+ const doc1 = deepClone<DeepPartial<typeof base>>(base)
866
+ const deletedUsersChanges = doc1.paths?.['/users']
867
+ const deletedPetsChanges = doc1.paths?.['/pets']
868
+ delete doc1.paths?.['/users']
869
+ delete doc1.paths?.['/pets']
870
+
871
+ const doc2 = {
872
+ ...base,
873
+ info: {
874
+ ...base.info,
875
+ },
876
+ paths: {
877
+ ...base.paths,
878
+ '/users': {
879
+ ...base.paths['/users'],
880
+ get: {
881
+ ...base.paths['/users'].get,
882
+ summary: 'Updated summary',
883
+ responses: {
884
+ ...base.paths['/users'].get.responses,
885
+ 200: {
886
+ ...base.paths['/users'].get.responses[200],
887
+ description: 'Updated Successful response',
888
+ },
889
+ 400: {
890
+ description: 'Error response',
891
+ },
892
+ },
893
+ },
894
+ },
895
+ '/pets': {
896
+ ...base.paths['/pets'],
897
+ get: {
898
+ ...base.paths['/pets'].get,
899
+ summary: 'Updated summary',
900
+ responses: {
901
+ ...base.paths['/pets'].get.responses,
902
+ 200: {
903
+ ...base.paths['/pets'].get.responses[200],
904
+ description: 'Updated Successful response',
905
+ },
906
+ 400: {
907
+ description: 'Error response',
908
+ },
909
+ },
910
+ },
911
+ },
912
+ },
913
+ }
914
+
915
+ expect(merge(diff(base, doc1), diff(base, doc2))).toEqual({
916
+ diffs: [],
917
+ conflicts: [
918
+ [
919
+ [
920
+ {
921
+ type: 'delete',
922
+ changes: deletedUsersChanges,
923
+ path: ['paths', '/users'],
924
+ },
925
+ ],
926
+ [
927
+ {
928
+ type: 'update',
929
+ changes: doc2.paths['/users'].get.summary,
930
+ path: ['paths', '/users', 'get', 'summary'],
931
+ },
932
+ {
933
+ type: 'update',
934
+ changes: doc2.paths['/users'].get.responses[200].description,
935
+ path: ['paths', '/users', 'get', 'responses', '200', 'description'],
936
+ },
937
+ {
938
+ type: 'add',
939
+ changes: doc2.paths['/users'].get.responses[400],
940
+ path: ['paths', '/users', 'get', 'responses', '400'],
941
+ },
942
+ ],
943
+ ],
944
+ [
945
+ [
946
+ {
947
+ type: 'delete',
948
+ changes: deletedPetsChanges,
949
+ path: ['paths', '/pets'],
950
+ },
951
+ ],
952
+ [
953
+ {
954
+ type: 'update',
955
+ changes: doc2.paths['/pets'].get.summary,
956
+ path: ['paths', '/pets', 'get', 'summary'],
957
+ },
958
+ {
959
+ type: 'update',
960
+ changes: doc2.paths['/pets'].get.responses[200].description,
961
+ path: ['paths', '/pets', 'get', 'responses', '200', 'description'],
962
+ },
963
+ {
964
+ type: 'add',
965
+ changes: doc2.paths['/pets'].get.responses[400],
966
+ path: ['paths', '/pets', 'get', 'responses', '400'],
967
+ },
968
+ ],
969
+ ],
970
+ ],
971
+ })
972
+
973
+ expect(merge(diff(base, doc2), diff(base, doc1))).toEqual({
974
+ diffs: [],
975
+ conflicts: [
976
+ [
977
+ [
978
+ {
979
+ type: 'update',
980
+ changes: doc2.paths['/users'].get.summary,
981
+ path: ['paths', '/users', 'get', 'summary'],
982
+ },
983
+ {
984
+ type: 'update',
985
+ changes: doc2.paths['/users'].get.responses[200].description,
986
+ path: ['paths', '/users', 'get', 'responses', '200', 'description'],
987
+ },
988
+ {
989
+ type: 'add',
990
+ changes: doc2.paths['/users'].get.responses[400],
991
+ path: ['paths', '/users', 'get', 'responses', '400'],
992
+ },
993
+ ],
994
+ [
995
+ {
996
+ type: 'delete',
997
+ changes: deletedUsersChanges,
998
+ path: ['paths', '/users'],
999
+ },
1000
+ ],
1001
+ ],
1002
+ [
1003
+ [
1004
+ {
1005
+ type: 'update',
1006
+ changes: doc2.paths['/pets'].get.summary,
1007
+ path: ['paths', '/pets', 'get', 'summary'],
1008
+ },
1009
+ {
1010
+ type: 'update',
1011
+ changes: doc2.paths['/pets'].get.responses[200].description,
1012
+ path: ['paths', '/pets', 'get', 'responses', '200', 'description'],
1013
+ },
1014
+ {
1015
+ type: 'add',
1016
+ changes: doc2.paths['/pets'].get.responses[400],
1017
+ path: ['paths', '/pets', 'get', 'responses', '400'],
1018
+ },
1019
+ ],
1020
+ [
1021
+ {
1022
+ type: 'delete',
1023
+ changes: deletedPetsChanges,
1024
+ path: ['paths', '/pets'],
1025
+ },
1026
+ ],
1027
+ ],
1028
+ ],
1029
+ })
1030
+ })
1031
+
1032
+ test('inner delete should not conflict with an outer update or addition', () => {
1033
+ const base = {
1034
+ openapi: '3.0.0',
1035
+ info: {
1036
+ title: 'Sample API',
1037
+ version: '1.0',
1038
+ },
1039
+ paths: {
1040
+ '/users': {
1041
+ get: {
1042
+ summary: 'Get users',
1043
+ responses: {
1044
+ 200: {
1045
+ description: 'Successful response',
1046
+ },
1047
+ },
1048
+ },
1049
+ },
1050
+ '/pets': {
1051
+ get: {
1052
+ summary: 'Get pets',
1053
+ responses: {
1054
+ 200: {
1055
+ description: 'Successful response',
1056
+ },
1057
+ },
1058
+ },
1059
+ },
1060
+ },
1061
+ }
1062
+
1063
+ const doc1 = deepClone<DeepPartial<typeof base>>(base)
1064
+ const deletedUsersChanges = doc1.paths?.['/users']?.get?.responses
1065
+ const deletedPetsChanges = doc1.paths?.['/pets']?.get?.responses
1066
+ delete doc1.paths?.['/users']?.get?.responses
1067
+ delete doc1.paths?.['/pets']?.get?.responses
1068
+
1069
+ const doc2 = {
1070
+ ...base,
1071
+ info: {
1072
+ ...base.info,
1073
+ },
1074
+ paths: {
1075
+ ...base.paths,
1076
+ '/users': {
1077
+ ...base.paths['/users'],
1078
+ post: {
1079
+ summary: 'Create a new user',
1080
+ },
1081
+ },
1082
+ '/pets': {
1083
+ ...base.paths['/pets'],
1084
+ },
1085
+ },
1086
+ }
1087
+
1088
+ expect(merge(diff(base, doc1), diff(base, doc2))).toEqual({
1089
+ diffs: [
1090
+ {
1091
+ type: 'delete',
1092
+ changes: deletedUsersChanges,
1093
+ path: ['paths', '/users', 'get', 'responses'],
1094
+ },
1095
+ {
1096
+ type: 'delete',
1097
+ changes: deletedPetsChanges,
1098
+ path: ['paths', '/pets', 'get', 'responses'],
1099
+ },
1100
+ {
1101
+ type: 'add',
1102
+ changes: doc2.paths['/users'].post,
1103
+ path: ['paths', '/users', 'post'],
1104
+ },
1105
+ ],
1106
+ conflicts: [],
1107
+ })
1108
+ })
1109
+ })