@scalar/json-magic 0.4.3 → 0.5.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 (106) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +10 -0
  3. package/dist/bundle/bundle.d.ts +0 -10
  4. package/dist/bundle/bundle.d.ts.map +1 -1
  5. package/dist/bundle/bundle.js +27 -29
  6. package/dist/bundle/bundle.js.map +2 -2
  7. package/dist/bundle/plugins/fetch-urls/index.d.ts.map +1 -1
  8. package/dist/bundle/plugins/fetch-urls/index.js +2 -2
  9. package/dist/bundle/plugins/fetch-urls/index.js.map +2 -2
  10. package/dist/bundle/plugins/parse-json/index.d.ts.map +1 -1
  11. package/dist/bundle/plugins/parse-json/index.js +1 -1
  12. package/dist/bundle/plugins/parse-json/index.js.map +2 -2
  13. package/dist/bundle/plugins/parse-yaml/index.d.ts.map +1 -1
  14. package/dist/bundle/plugins/parse-yaml/index.js +1 -1
  15. package/dist/bundle/plugins/parse-yaml/index.js.map +2 -2
  16. package/dist/bundle/plugins/read-files/index.d.ts.map +1 -1
  17. package/dist/bundle/plugins/read-files/index.js +1 -1
  18. package/dist/bundle/plugins/read-files/index.js.map +2 -2
  19. package/dist/helpers/convert-to-local-ref.d.ts +10 -0
  20. package/dist/helpers/convert-to-local-ref.d.ts.map +1 -0
  21. package/dist/helpers/convert-to-local-ref.js +26 -0
  22. package/dist/helpers/convert-to-local-ref.js.map +7 -0
  23. package/dist/helpers/escape-json-pointer.d.ts.map +1 -0
  24. package/dist/{utils → helpers}/escape-json-pointer.js.map +1 -1
  25. package/dist/helpers/get-schemas.d.ts +21 -0
  26. package/dist/helpers/get-schemas.d.ts.map +1 -0
  27. package/dist/helpers/get-schemas.js +37 -0
  28. package/dist/helpers/get-schemas.js.map +7 -0
  29. package/dist/helpers/get-segments-from-path.d.ts.map +1 -0
  30. package/dist/{utils → helpers}/get-segments-from-path.js.map +1 -1
  31. package/dist/helpers/get-value-by-path.d.ts +24 -0
  32. package/dist/helpers/get-value-by-path.d.ts.map +1 -0
  33. package/dist/helpers/get-value-by-path.js +23 -0
  34. package/dist/helpers/get-value-by-path.js.map +7 -0
  35. package/dist/helpers/is-json-object.d.ts.map +1 -0
  36. package/dist/{utils → helpers}/is-json-object.js +1 -1
  37. package/dist/helpers/is-json-object.js.map +7 -0
  38. package/dist/helpers/is-object.d.ts.map +1 -0
  39. package/dist/{utils → helpers}/is-object.js.map +1 -1
  40. package/dist/helpers/is-yaml.d.ts.map +1 -0
  41. package/dist/{utils → helpers}/is-yaml.js.map +1 -1
  42. package/dist/{utils → helpers}/json-path-utils.d.ts +0 -11
  43. package/dist/helpers/json-path-utils.d.ts.map +1 -0
  44. package/dist/{utils → helpers}/json-path-utils.js +0 -9
  45. package/dist/helpers/json-path-utils.js.map +7 -0
  46. package/dist/helpers/normalize.d.ts.map +1 -0
  47. package/dist/{utils → helpers}/normalize.js.map +1 -1
  48. package/dist/helpers/unescape-json-pointer.d.ts.map +1 -0
  49. package/dist/{utils → helpers}/unescape-json-pointer.js.map +1 -1
  50. package/dist/magic-proxy/proxy.d.ts +35 -10
  51. package/dist/magic-proxy/proxy.d.ts.map +1 -1
  52. package/dist/magic-proxy/proxy.js +40 -20
  53. package/dist/magic-proxy/proxy.js.map +2 -2
  54. package/esbuild.ts +1 -0
  55. package/package.json +6 -1
  56. package/src/bundle/bundle.test.ts +462 -25
  57. package/src/bundle/bundle.ts +37 -36
  58. package/src/bundle/plugins/fetch-urls/index.ts +2 -2
  59. package/src/bundle/plugins/parse-json/index.ts +1 -1
  60. package/src/bundle/plugins/parse-yaml/index.ts +3 -2
  61. package/src/bundle/plugins/read-files/index.ts +1 -1
  62. package/src/helpers/convert-to-local-ref.test.ts +211 -0
  63. package/src/helpers/convert-to-local-ref.ts +43 -0
  64. package/src/helpers/get-schemas.test.ts +356 -0
  65. package/src/helpers/get-schemas.ts +80 -0
  66. package/src/helpers/get-value-by-path.test.ts +338 -0
  67. package/src/helpers/get-value-by-path.ts +44 -0
  68. package/src/{utils → helpers}/is-json-object.ts +1 -1
  69. package/src/{utils → helpers}/json-path-utils.ts +0 -19
  70. package/src/{utils → helpers}/normalize.test.ts +2 -1
  71. package/src/magic-proxy/proxy.test.ts +548 -0
  72. package/src/magic-proxy/proxy.ts +80 -31
  73. package/dist/utils/escape-json-pointer.d.ts.map +0 -1
  74. package/dist/utils/get-segments-from-path.d.ts.map +0 -1
  75. package/dist/utils/is-json-object.d.ts.map +0 -1
  76. package/dist/utils/is-json-object.js.map +0 -7
  77. package/dist/utils/is-object.d.ts.map +0 -1
  78. package/dist/utils/is-yaml.d.ts.map +0 -1
  79. package/dist/utils/json-path-utils.d.ts.map +0 -1
  80. package/dist/utils/json-path-utils.js.map +0 -7
  81. package/dist/utils/normalize.d.ts.map +0 -1
  82. package/dist/utils/unescape-json-pointer.d.ts.map +0 -1
  83. /package/dist/{utils → helpers}/escape-json-pointer.d.ts +0 -0
  84. /package/dist/{utils → helpers}/escape-json-pointer.js +0 -0
  85. /package/dist/{utils → helpers}/get-segments-from-path.d.ts +0 -0
  86. /package/dist/{utils → helpers}/get-segments-from-path.js +0 -0
  87. /package/dist/{utils → helpers}/is-json-object.d.ts +0 -0
  88. /package/dist/{utils → helpers}/is-object.d.ts +0 -0
  89. /package/dist/{utils → helpers}/is-object.js +0 -0
  90. /package/dist/{utils → helpers}/is-yaml.d.ts +0 -0
  91. /package/dist/{utils → helpers}/is-yaml.js +0 -0
  92. /package/dist/{utils → helpers}/normalize.d.ts +0 -0
  93. /package/dist/{utils → helpers}/normalize.js +0 -0
  94. /package/dist/{utils → helpers}/unescape-json-pointer.d.ts +0 -0
  95. /package/dist/{utils → helpers}/unescape-json-pointer.js +0 -0
  96. /package/src/{utils → helpers}/escape-json-pointer.test.ts +0 -0
  97. /package/src/{utils → helpers}/escape-json-pointer.ts +0 -0
  98. /package/src/{utils → helpers}/get-segments-from-path.test.ts +0 -0
  99. /package/src/{utils → helpers}/get-segments-from-path.ts +0 -0
  100. /package/src/{utils → helpers}/is-object.test.ts +0 -0
  101. /package/src/{utils → helpers}/is-object.ts +0 -0
  102. /package/src/{utils → helpers}/is-yaml.ts +0 -0
  103. /package/src/{utils → helpers}/json-path-utils.test.ts +0 -0
  104. /package/src/{utils → helpers}/normalize.ts +0 -0
  105. /package/src/{utils → helpers}/unescape-json-pointer.test.ts +0 -0
  106. /package/src/{utils → helpers}/unescape-json-pointer.ts +0 -0
@@ -0,0 +1,356 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { getId, getSchemas } from './get-schemas'
4
+
5
+ describe('getId', () => {
6
+ it('returns the $id string when present', () => {
7
+ const input = { $id: 'https://example.com/schema' }
8
+ const result = getId(input)
9
+ expect(result).toBe('https://example.com/schema')
10
+ })
11
+
12
+ it('returns undefined when $id is not present', () => {
13
+ const input = { name: 'test' }
14
+ const result = getId(input)
15
+ expect(result).toBeUndefined()
16
+ })
17
+
18
+ it('returns undefined when $id is not a string', () => {
19
+ const input = { $id: 123 }
20
+ const result = getId(input)
21
+ expect(result).toBeUndefined()
22
+ })
23
+
24
+ it('returns undefined when $id is null', () => {
25
+ const input = { $id: null }
26
+ const result = getId(input)
27
+ expect(result).toBeUndefined()
28
+ })
29
+
30
+ it('returns undefined when $id is an object', () => {
31
+ const input = { $id: { nested: 'value' } }
32
+ const result = getId(input)
33
+ expect(result).toBeUndefined()
34
+ })
35
+
36
+ it('returns undefined when $id is an array', () => {
37
+ const input = { $id: ['item1', 'item2'] }
38
+ const result = getId(input)
39
+ expect(result).toBeUndefined()
40
+ })
41
+
42
+ it('returns undefined when $id is a boolean', () => {
43
+ const input = { $id: true }
44
+ const result = getId(input)
45
+ expect(result).toBeUndefined()
46
+ })
47
+
48
+ it('returns undefined when $id is an empty string', () => {
49
+ const input = { $id: '' }
50
+ const result = getId(input)
51
+ expect(result).toBe(undefined)
52
+ })
53
+ })
54
+
55
+ describe('getSchemas', () => {
56
+ it('returns empty map for non-object input', () => {
57
+ const result = getSchemas('string')
58
+ expect(result.size).toBe(0)
59
+ })
60
+
61
+ it('returns empty map for null input', () => {
62
+ const result = getSchemas(null)
63
+ expect(result.size).toBe(0)
64
+ })
65
+
66
+ it('returns empty map for undefined input', () => {
67
+ const result = getSchemas(undefined)
68
+ expect(result.size).toBe(0)
69
+ })
70
+
71
+ it('returns empty map for number input', () => {
72
+ const result = getSchemas(42)
73
+ expect(result.size).toBe(0)
74
+ })
75
+
76
+ it('returns empty map for boolean input', () => {
77
+ const result = getSchemas(true)
78
+ expect(result.size).toBe(0)
79
+ })
80
+
81
+ it('returns empty map for array input', () => {
82
+ const result = getSchemas([1, 2, 3])
83
+ expect(result.size).toBe(0)
84
+ })
85
+
86
+ it('collects schema with $id', () => {
87
+ const input = {
88
+ $id: 'https://example.com/schema',
89
+ type: 'object',
90
+ }
91
+ const result = getSchemas(input)
92
+ expect(result.size).toBe(1)
93
+ expect(result.get('https://example.com/schema')).toBe('')
94
+ })
95
+
96
+ it('collects schema with $anchor', () => {
97
+ const input = {
98
+ $anchor: 'myAnchor',
99
+ type: 'string',
100
+ }
101
+ const result = getSchemas(input)
102
+ expect(result.size).toBe(1)
103
+ expect(result.get('#myAnchor')).toBe('')
104
+ })
105
+
106
+ it('collects schema with both $id and $anchor', () => {
107
+ const input = {
108
+ $id: 'https://example.com/schema',
109
+ $anchor: 'myAnchor',
110
+ type: 'object',
111
+ }
112
+ const result = getSchemas(input)
113
+ expect(result.size).toBe(2)
114
+ expect(result.get('https://example.com/schema')).toBe('')
115
+ expect(result.get('https://example.com/schema#myAnchor')).toBe('')
116
+ })
117
+
118
+ it('collects nested schemas with $id', () => {
119
+ const input = {
120
+ definitions: {
121
+ user: {
122
+ $id: 'https://example.com/user',
123
+ type: 'object',
124
+ },
125
+ product: {
126
+ $id: 'https://example.com/product',
127
+ type: 'object',
128
+ },
129
+ },
130
+ }
131
+ const result = getSchemas(input)
132
+ expect(result.size).toBe(2)
133
+ expect(result.get('https://example.com/user')).toBe('definitions/user')
134
+ expect(result.get('https://example.com/product')).toBe('definitions/product')
135
+ })
136
+
137
+ it('collects nested schemas with $anchor', () => {
138
+ const input = {
139
+ definitions: {
140
+ user: {
141
+ $anchor: 'user',
142
+ type: 'object',
143
+ },
144
+ product: {
145
+ $anchor: 'product',
146
+ type: 'object',
147
+ },
148
+ },
149
+ }
150
+ const result = getSchemas(input)
151
+ expect(result.size).toBe(2)
152
+ expect(result.get('#user')).toBe('definitions/user')
153
+ expect(result.get('#product')).toBe('definitions/product')
154
+ })
155
+
156
+ it('uses parent $id as base for nested $anchor', () => {
157
+ const input = {
158
+ $id: 'https://example.com/schema',
159
+ definitions: {
160
+ user: {
161
+ $anchor: 'user',
162
+ type: 'object',
163
+ },
164
+ },
165
+ }
166
+ const result = getSchemas(input)
167
+ expect(result.size).toBe(2)
168
+ expect(result.get('https://example.com/schema')).toBe('')
169
+ expect(result.get('https://example.com/schema#user')).toBe('definitions/user')
170
+ })
171
+
172
+ it('handles deeply nested schemas', () => {
173
+ const input = {
174
+ components: {
175
+ schemas: {
176
+ user: {
177
+ $id: 'https://example.com/user',
178
+ properties: {
179
+ address: {
180
+ $anchor: 'address',
181
+ type: 'object',
182
+ },
183
+ },
184
+ },
185
+ },
186
+ },
187
+ }
188
+ const result = getSchemas(input)
189
+ expect(result.size).toBe(2)
190
+ expect(result.get('https://example.com/user')).toBe('components/schemas/user')
191
+ expect(result.get('https://example.com/user#address')).toBe('components/schemas/user/properties/address')
192
+ })
193
+
194
+ it('handles circular references without infinite loops', () => {
195
+ const input: any = {
196
+ $id: 'https://example.com/schema',
197
+ type: 'object',
198
+ }
199
+ input.self = input // Create circular reference
200
+
201
+ const result = getSchemas(input)
202
+ expect(result.size).toBe(1)
203
+ expect(result.get('https://example.com/schema')).toBe('')
204
+ })
205
+
206
+ it('handles multiple circular references', () => {
207
+ const input: any = {
208
+ $id: 'https://example.com/schema',
209
+ type: 'object',
210
+ properties: {
211
+ user: {
212
+ $anchor: 'user',
213
+ type: 'object',
214
+ },
215
+ },
216
+ }
217
+ input.self = input
218
+ input.properties.user.self = input
219
+
220
+ const result = getSchemas(input)
221
+ expect(result.size).toBe(2)
222
+ expect(result.get('https://example.com/schema')).toBe('')
223
+ expect(result.get('https://example.com/schema#user')).toBe('properties/user')
224
+ })
225
+
226
+ it('ignores non-string $id values', () => {
227
+ const input = {
228
+ $id: 123,
229
+ type: 'object',
230
+ }
231
+ const result = getSchemas(input)
232
+ expect(result.size).toBe(0)
233
+ })
234
+
235
+ it('ignores non-string $anchor values', () => {
236
+ const input = {
237
+ $anchor: 123,
238
+ type: 'object',
239
+ }
240
+ const result = getSchemas(input)
241
+ expect(result.size).toBe(0)
242
+ })
243
+
244
+ it('handles empty objects', () => {
245
+ const input = {}
246
+ const result = getSchemas(input)
247
+ expect(result.size).toBe(0)
248
+ })
249
+
250
+ it('handles objects with only primitive values', () => {
251
+ const input = {
252
+ name: 'test',
253
+ age: 25,
254
+ active: true,
255
+ tags: ['tag1', 'tag2'],
256
+ }
257
+ const result = getSchemas(input)
258
+ expect(result.size).toBe(0)
259
+ })
260
+
261
+ it('handles mixed primitive and object values', () => {
262
+ const input = {
263
+ name: 'test',
264
+ schema: {
265
+ $id: 'https://example.com/schema',
266
+ type: 'object',
267
+ },
268
+ age: 25,
269
+ }
270
+ const result = getSchemas(input)
271
+ expect(result.size).toBe(1)
272
+ expect(result.get('https://example.com/schema')).toBe('schema')
273
+ })
274
+
275
+ it('preserves custom base parameter', () => {
276
+ const input = {
277
+ $anchor: 'myAnchor',
278
+ type: 'object',
279
+ }
280
+ const result = getSchemas(input, 'https://custom.com/base')
281
+ expect(result.size).toBe(1)
282
+ expect(result.get('https://custom.com/base#myAnchor')).toBe('')
283
+ })
284
+
285
+ it('preserves custom segments parameter', () => {
286
+ const input = {
287
+ $id: 'https://example.com/schema',
288
+ type: 'object',
289
+ }
290
+ const result = getSchemas(input, '', ['custom', 'path'])
291
+ expect(result.size).toBe(1)
292
+ expect(result.get('https://example.com/schema')).toBe('custom/path')
293
+ })
294
+
295
+ it('reuses existing map when provided', () => {
296
+ const existingMap = new Map([['existing', 'path']])
297
+ const input = {
298
+ $id: 'https://example.com/schema',
299
+ type: 'object',
300
+ }
301
+ const result = getSchemas(input, '', [], existingMap)
302
+ expect(result).toBe(existingMap)
303
+ expect(result.size).toBe(2)
304
+ expect(result.get('existing')).toBe('path')
305
+ expect(result.get('https://example.com/schema')).toBe('')
306
+ })
307
+
308
+ it('handles complex nested structure with multiple schemas', () => {
309
+ const input = {
310
+ $id: 'https://example.com/root',
311
+ openapi: '3.0.0',
312
+ info: {
313
+ title: 'API',
314
+ },
315
+ components: {
316
+ schemas: {
317
+ user: {
318
+ $id: 'https://example.com/user',
319
+ $anchor: 'user',
320
+ type: 'object',
321
+ properties: {
322
+ name: {
323
+ type: 'string',
324
+ },
325
+ address: {
326
+ $anchor: 'address',
327
+ type: 'object',
328
+ properties: {
329
+ street: {
330
+ type: 'string',
331
+ },
332
+ },
333
+ },
334
+ },
335
+ },
336
+ product: {
337
+ $anchor: 'product',
338
+ type: 'object',
339
+ properties: {
340
+ id: {
341
+ type: 'integer',
342
+ },
343
+ },
344
+ },
345
+ },
346
+ },
347
+ }
348
+ const result = getSchemas(input)
349
+ expect(result.size).toBe(5)
350
+ expect(result.get('https://example.com/root')).toBe('')
351
+ expect(result.get('https://example.com/user')).toBe('components/schemas/user')
352
+ expect(result.get('https://example.com/user#user')).toBe('components/schemas/user')
353
+ expect(result.get('https://example.com/user#address')).toBe('components/schemas/user/properties/address')
354
+ expect(result.get('https://example.com/root#product')).toBe('components/schemas/product')
355
+ })
356
+ })
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Retrieves the $id property from the input object if it exists and is a string.
3
+ *
4
+ * @param input - The object to extract the $id from.
5
+ * @returns The $id string if present, otherwise undefined.
6
+ */
7
+ export const getId = (input: unknown): string | undefined => {
8
+ if (input && typeof input === 'object' && input['$id'] && typeof input['$id'] === 'string') {
9
+ return input['$id']
10
+ }
11
+ return undefined
12
+ }
13
+
14
+ /**
15
+ * Joins an array of path segments into a single string separated by '/'.
16
+ *
17
+ * @param segments - The array of path segments.
18
+ * @returns The joined path string.
19
+ */
20
+ const getPath = (segments: string[]): string => {
21
+ return segments.join('/')
22
+ }
23
+
24
+ /**
25
+ * Recursively traverses the input object to collect all schemas identified by $id and $anchor properties.
26
+ *
27
+ * - If an object has a $id property, it is added to the map with its $id as the key.
28
+ * - If an object has a $anchor property, it is added to the map with a key composed of the current base and the anchor.
29
+ * - The function performs a depth-first search (DFS) through all nested objects.
30
+ *
31
+ * @param input - The input object to traverse.
32
+ * @param base - The current base URI, used for resolving anchors.
33
+ * @param map - The map collecting found schemas.
34
+ * @returns A map of schema identifiers to their corresponding objects.
35
+ */
36
+ export const getSchemas = (
37
+ input: unknown,
38
+ base: string = '',
39
+ segments: string[] = [],
40
+ map = new Map<string, string>(),
41
+ visited = new WeakSet(),
42
+ ) => {
43
+ // Only process non-null objects
44
+ if (typeof input !== 'object' || input === null) {
45
+ return map
46
+ }
47
+
48
+ // If the object has already been visited, return the map
49
+ if (visited.has(input)) {
50
+ return map
51
+ }
52
+
53
+ // Add the object to the visited set
54
+ visited.add(input)
55
+
56
+ // Attempt to get $id from the current object
57
+ const id = getId(input)
58
+
59
+ // If $id exists, add the object to the map with $id as the key
60
+ if (id) {
61
+ map.set(id, getPath(segments))
62
+ }
63
+
64
+ // Update the base for nested anchors
65
+ const newBase = id ?? base
66
+
67
+ // If $anchor exists, add the object to the map with base#anchor as the key
68
+ if (input['$anchor'] && typeof input['$anchor'] === 'string') {
69
+ map.set(`${newBase}#${input['$anchor']}`, getPath(segments))
70
+ }
71
+
72
+ // Recursively traverse all properties (DFS)
73
+ for (const key in input) {
74
+ if (typeof input[key] === 'object' && input[key] !== null) {
75
+ getSchemas(input[key], newBase, [...segments, key], map, visited)
76
+ }
77
+ }
78
+
79
+ return map
80
+ }