@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,166 @@
1
+ import { generateUniqueValue, uniqueValueGeneratorFactory } from './value-generator'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ describe('generateUniqueHash', () => {
5
+ it('should generate hash values from the function we pass in', async () => {
6
+ const hashFunction = async (value: string) => {
7
+ if (value === 'a') {
8
+ return 'a'
9
+ }
10
+ throw 'Ops'
11
+ }
12
+
13
+ const result = await generateUniqueValue(hashFunction, 'a', {})
14
+
15
+ expect(result).toBe('a')
16
+ })
17
+
18
+ it('should return same value for the same input', async () => {
19
+ const hashFunction = async (value: string) => {
20
+ if (value === 'a') {
21
+ return value
22
+ }
23
+ throw 'Ops'
24
+ }
25
+
26
+ const map = {}
27
+
28
+ const result1 = await generateUniqueValue(hashFunction, 'a', map)
29
+ expect(result1).toBe('a')
30
+
31
+ const result2 = await generateUniqueValue(hashFunction, 'a', map)
32
+ expect(result2).toBe('a')
33
+ })
34
+
35
+ it('should handle hash collisions', async () => {
36
+ const hashFunction = async (value: string) => {
37
+ // Hash a, b and c will produce collisions
38
+ if (value === 'a' || value === 'b' || value === 'c') {
39
+ return 'd'
40
+ }
41
+
42
+ // Hashing the hashed result
43
+ if (value === 'd') {
44
+ return 'e'
45
+ }
46
+
47
+ if (value === 'e') {
48
+ return 'f'
49
+ }
50
+
51
+ if (value === 'f') {
52
+ return 'g'
53
+ }
54
+
55
+ if (value === 'g') {
56
+ return 'h'
57
+ }
58
+
59
+ throw 'Ops'
60
+ }
61
+
62
+ const map = {}
63
+
64
+ const res1 = await generateUniqueValue(hashFunction, 'd', map)
65
+ expect(res1).toBe('e')
66
+
67
+ const res2 = await generateUniqueValue(hashFunction, 'b', map)
68
+ expect(res2).toBe('d')
69
+
70
+ // Takes the first available spot
71
+ const res3 = await generateUniqueValue(hashFunction, 'a', map)
72
+ expect(res3).toBe('f')
73
+
74
+ // Produces the same output for the same input
75
+ const res4 = await generateUniqueValue(hashFunction, 'a', map)
76
+ expect(res4).toBe('f')
77
+
78
+ const res5 = await generateUniqueValue(hashFunction, 'c', map)
79
+ expect(res5).toBe('g')
80
+
81
+ const res6 = await generateUniqueValue(hashFunction, 'g', map)
82
+ expect(res6).toBe('h')
83
+ })
84
+
85
+ it('should throw when it reaches max depth', async () => {
86
+ const hashFunction = async () => {
87
+ return 'a'
88
+ }
89
+
90
+ const map = {}
91
+ const result1 = await generateUniqueValue(hashFunction, 'a', map)
92
+ expect(result1).toBe('a')
93
+
94
+ const result2 = await generateUniqueValue(hashFunction, 'a', map)
95
+ expect(result2).toBe('a')
96
+
97
+ expect(() => generateUniqueValue(hashFunction, 'b', map)).rejects.toThrowError()
98
+ })
99
+ })
100
+
101
+ describe('uniqueValueGeneratorFactory', () => {
102
+ it('should generate unique values while caching the previously generated values', async () => {
103
+ let calls = 0
104
+ const compress = (value: string) => {
105
+ // Ensure we return the same value only once for the input 'b'
106
+ if (value === 'b' && calls === 0) {
107
+ calls++
108
+ return 'another-value'
109
+ }
110
+ calls++
111
+ return 'should not be returned'
112
+ }
113
+ const { generate } = uniqueValueGeneratorFactory(compress, {
114
+ 'b': 'a',
115
+ })
116
+
117
+ // Should use the value from the cache
118
+ expect(await generate('a')).toBe('b')
119
+
120
+ // should use the generator function to generate a unique value
121
+ expect(await generate('b')).toBe('another-value')
122
+
123
+ // should generate the same value when we call generate with the same input
124
+ expect(await generate('b')).toBe('another-value')
125
+ })
126
+
127
+ it('should handle conflicts', async () => {
128
+ const compress = (value: string) => {
129
+ if (value === 'a') {
130
+ return 'c'
131
+ }
132
+
133
+ if (value === 'b') {
134
+ return 'c'
135
+ }
136
+
137
+ if (value === 'c') {
138
+ return 'd'
139
+ }
140
+ return 'should not be returned'
141
+ }
142
+ const { generate } = uniqueValueGeneratorFactory(compress, {})
143
+
144
+ expect(await generate('a')).toBe('c')
145
+ expect(await generate('b')).toBe('d')
146
+ })
147
+
148
+ it('should handle numeric strings by prefixing them with letters', async () => {
149
+ const compress = (value: string) => {
150
+ if (!value) {
151
+ return '92819102'
152
+ }
153
+ return 'radom value'
154
+ }
155
+
156
+ const { generate } = uniqueValueGeneratorFactory(compress, {})
157
+
158
+ // Prefix with a letter
159
+ expect(await generate('')).toBe('a92819102')
160
+
161
+ // Should return the same value for same input
162
+ expect(await generate('')).toBe('a92819102')
163
+
164
+ expect(await generate('abc')).toBe('radom value')
165
+ })
166
+ })
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Generates a short SHA-1 hash from a string value.
3
+ * This function is used to create unique identifiers for external references
4
+ * while keeping the hash length manageable. It uses the Web Crypto API to
5
+ * generate a SHA-1 hash and returns the first 7 characters of the hex string.
6
+ * If the hash would be all numbers, it ensures at least one letter is included.
7
+ *
8
+ * @param value - The string to hash
9
+ * @returns A 7-character hexadecimal hash with at least one letter
10
+ * @example
11
+ * // Returns "2ae91d7"
12
+ * await getHash("https://example.com/schema.json")
13
+ */
14
+ export async function getHash(value: string) {
15
+ // Convert string to ArrayBuffer
16
+ const encoder = new TextEncoder()
17
+ const data = encoder.encode(value)
18
+
19
+ // Hash the data
20
+ const hashBuffer = await crypto.subtle.digest('SHA-1', data)
21
+
22
+ // Convert buffer to hex string
23
+ const hashArray = Array.from(new Uint8Array(hashBuffer))
24
+ const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
25
+
26
+ // Return first 7 characters of the hash, ensuring at least one letter
27
+ const hash = hashHex.substring(0, 7)
28
+ return hash.match(/^\d+$/) ? 'a' + hash.substring(1) : hash
29
+ }
30
+
31
+ /**
32
+ * Generates a unique compressed value for a string, handling collisions by recursively compressing
33
+ * until a unique value is found. This is used to create unique identifiers for external
34
+ * references in the bundled OpenAPI document.
35
+ *
36
+ * @param compress - Function that generates a compressed value from a string
37
+ * @param value - The original string value to compress
38
+ * @param compressedToValue - Object mapping compressed values to their original values
39
+ * @param prevCompressedValue - Optional previous compressed value to use as input for generating a new value
40
+ * @param depth - Current recursion depth to prevent infinite loops
41
+ * @returns A unique compressed value that doesn't conflict with existing values
42
+ *
43
+ * @example
44
+ * const valueMap = {}
45
+ * // First call generates compressed value for "example.com/schema.json"
46
+ * const value1 = await generateUniqueValue(compress, "example.com/schema.json", valueMap)
47
+ * // Returns something like "2ae91d7"
48
+ *
49
+ * // Second call with same value returns same compressed value
50
+ * const value2 = await generateUniqueValue(compress, "example.com/schema.json", valueMap)
51
+ * // Returns same value as value1
52
+ *
53
+ * // Call with different value generates new unique compressed value
54
+ * const value3 = await generateUniqueValue(compress, "example.com/other.json", valueMap)
55
+ * // Returns different value like "3bf82e9"
56
+ */
57
+ export async function generateUniqueValue(
58
+ compress: (value: string) => Promise<string> | string,
59
+ value: string,
60
+ compressedToValue: Record<string, string>,
61
+ prevCompressedValue?: string,
62
+ depth = 0,
63
+ ) {
64
+ // Prevent infinite recursion by limiting depth
65
+ const MAX_DEPTH = 100
66
+
67
+ if (depth >= MAX_DEPTH) {
68
+ throw 'Can not generate unique compressed values'
69
+ }
70
+
71
+ // Compress the value, using previous compressed value if provided
72
+ const compressedValue = await compress(prevCompressedValue ?? value)
73
+
74
+ // Handle collision by recursively trying with compressed value as input
75
+ if (compressedToValue[compressedValue] !== undefined && compressedToValue[compressedValue] !== value) {
76
+ return generateUniqueValue(compress, value, compressedToValue, compressedValue, depth + 1)
77
+ }
78
+
79
+ // Store mapping and return unique compressed value
80
+ compressedToValue[compressedValue] = value
81
+ return compressedValue
82
+ }
83
+
84
+ /**
85
+ * Factory function that creates a value generator with caching capabilities.
86
+ * The generator maintains a bidirectional mapping between original values and their compressed forms.
87
+ *
88
+ * @param compress - Function that generates a compressed value from a string
89
+ * @param compressedToValue - Initial mapping of compressed values to their original values
90
+ * @returns An object with a generate method that produces unique compressed values
91
+ *
92
+ * @example
93
+ * const compress = (value) => value.substring(0, 6) // Simple compression example
94
+ * const initialMap = { 'abc123': 'example.com/schema.json' }
95
+ * const generator = uniqueValueGeneratorFactory(compress, initialMap)
96
+ *
97
+ * // Generate compressed value for new string
98
+ * const compressed = await generator.generate('example.com/other.json')
99
+ * // Returns something like 'example'
100
+ *
101
+ * // Generate compressed value for existing string
102
+ * const cached = await generator.generate('example.com/schema.json')
103
+ * // Returns 'abc123' from cache
104
+ */
105
+ export const uniqueValueGeneratorFactory = (
106
+ compress: (value: string) => Promise<string> | string,
107
+ compressedToValue: Record<string, string>,
108
+ ) => {
109
+ // Create a reverse mapping from original values to their compressed forms
110
+ const valueToCompressed = Object.fromEntries(Object.entries(compressedToValue).map(([key, value]) => [value, key]))
111
+
112
+ return {
113
+ /**
114
+ * Generates a unique compressed value for the given input string.
115
+ * First checks if a compressed value already exists in the cache.
116
+ * If not, generates a new unique compressed value and stores it in the cache.
117
+ *
118
+ * @param value - The original string value to compress
119
+ * @returns A Promise that resolves to the compressed string value
120
+ *
121
+ * @example
122
+ * const generator = uniqueValueGeneratorFactory(compress, {})
123
+ * const compressed = await generator.generate('example.com/schema.json')
124
+ * // Returns a unique compressed value like 'example'
125
+ */
126
+ generate: async (value: string) => {
127
+ // Check if we already have a compressed value for this input
128
+ const cache = valueToCompressed[value]
129
+ if (cache) {
130
+ return cache
131
+ }
132
+
133
+ // Generate a new unique compressed value
134
+ const generatedValue = await generateUniqueValue(compress, value, compressedToValue)
135
+
136
+ // Ensure the generated string contains at least one non-numeric character
137
+ // This prevents the `setValueAtPath` function from interpreting the value as an array index
138
+ // by forcing it to be treated as an object property name
139
+ const compressedValue = generatedValue.match(/^\d+$/) ? `a${generatedValue}` : generatedValue
140
+
141
+ // Store the new mapping in our cache
142
+ valueToCompressed[value] = compressedValue
143
+
144
+ return compressedValue
145
+ },
146
+ }
147
+ }
@@ -0,0 +1,137 @@
1
+ import { dereference } from '@/dereference/dereference'
2
+ import { fastify, type FastifyInstance } from 'fastify'
3
+ import { setTimeout } from 'node:timers/promises'
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
5
+
6
+ describe('dereference', () => {
7
+ describe('sync', () => {
8
+ it('should dereference JSON pointers', async () => {
9
+ const data = {
10
+ users: {
11
+ name: 'John Doe',
12
+ age: 30,
13
+ address: {
14
+ city: 'New York',
15
+ street: '5th Avenue',
16
+ },
17
+ },
18
+ profile: {
19
+ $ref: '#/users',
20
+ },
21
+ address: {
22
+ $ref: '#/users/address',
23
+ },
24
+ }
25
+
26
+ const result = dereference(data, { sync: true })
27
+
28
+ expect(result).toEqual({
29
+ success: true,
30
+ data: {
31
+ users: {
32
+ name: 'John Doe',
33
+ age: 30,
34
+ address: {
35
+ city: 'New York',
36
+ street: '5th Avenue',
37
+ },
38
+ },
39
+ profile: {
40
+ 'x-original-ref': '#/users',
41
+ name: 'John Doe',
42
+ age: 30,
43
+ address: {
44
+ city: 'New York',
45
+ street: '5th Avenue',
46
+ },
47
+ },
48
+ address: {
49
+ 'x-original-ref': '#/users/address',
50
+ city: 'New York',
51
+ street: '5th Avenue',
52
+ },
53
+ },
54
+ })
55
+ })
56
+ })
57
+
58
+ describe('async', () => {
59
+ let server: FastifyInstance
60
+ const port = 7299
61
+ const url = `http://localhost:${port}`
62
+
63
+ beforeEach(() => {
64
+ server = fastify({ logger: false })
65
+ })
66
+
67
+ afterEach(async () => {
68
+ await server.close()
69
+ await setTimeout(100)
70
+ })
71
+
72
+ it('should dereference JSON pointers asynchronously', async () => {
73
+ const userProfile = {
74
+ name: 'Jane Doe',
75
+ age: 25,
76
+ address: {
77
+ city: 'Los Angeles',
78
+ street: 'Sunset Boulevard',
79
+ },
80
+ }
81
+ server.get('/users', async () => {
82
+ return userProfile
83
+ })
84
+
85
+ await server.listen({ port: port })
86
+
87
+ const data = {
88
+ profile: {
89
+ $ref: `${url}/users#`,
90
+ },
91
+ address: {
92
+ $ref: `${url}/users#/address`,
93
+ },
94
+ }
95
+ const result = await dereference(data, { sync: false })
96
+ expect(result).toEqual({
97
+ success: true,
98
+ data: {
99
+ profile: {
100
+ 'x-original-ref': '#/x-ext/5bd1cdd',
101
+ name: 'Jane Doe',
102
+ age: 25,
103
+ address: {
104
+ city: 'Los Angeles',
105
+ street: 'Sunset Boulevard',
106
+ },
107
+ },
108
+ address: {
109
+ 'x-original-ref': '#/x-ext/5bd1cdd/address',
110
+ city: 'Los Angeles',
111
+ street: 'Sunset Boulevard',
112
+ },
113
+ 'x-ext': {
114
+ '5bd1cdd': userProfile,
115
+ },
116
+ 'x-ext-urls': {
117
+ '5bd1cdd': `${url}/users`,
118
+ },
119
+ },
120
+ })
121
+ })
122
+
123
+ it('should handle errors when dereferencing remote refs', async () => {
124
+ const data = {
125
+ profile: {
126
+ $ref: `${url}/nonexistent`,
127
+ },
128
+ }
129
+
130
+ const result = await dereference(data, { sync: false })
131
+ expect(result).toEqual({
132
+ success: false,
133
+ errors: [`Failed to resolve ${url}/nonexistent`],
134
+ })
135
+ })
136
+ })
137
+ })
@@ -0,0 +1,84 @@
1
+ import { bundle } from '@/bundle'
2
+ import { fetchUrls } from '@/bundle/plugins/fetch-urls'
3
+ import { createMagicProxy } from '@/magic-proxy'
4
+ import type { UnknownObject } from '@/types'
5
+
6
+ type DereferenceResult =
7
+ | {
8
+ success: true
9
+ data: UnknownObject
10
+ }
11
+ | {
12
+ success: false
13
+ errors: string[]
14
+ }
15
+
16
+ type ReturnDereferenceResult<Opt extends { sync?: boolean }> = Opt['sync'] extends true
17
+ ? DereferenceResult
18
+ : Promise<DereferenceResult>
19
+
20
+ /**
21
+ * Dereferences a JSON object, resolving all $ref pointers.
22
+ *
23
+ * This function can operate synchronously (no remote refs, no async plugins) or asynchronously (with remote refs).
24
+ * If `options.sync` is true, it simply wraps the input in a magic proxy and returns it.
25
+ * Otherwise, it bundles the document, resolving all $refs (including remote ones), and returns a promise.
26
+ *
27
+ * @param input - JSON Schema object to dereference.
28
+ * @param options - Optional settings. If `sync` is true, dereferencing is synchronous.
29
+ * @returns A DereferenceResult (or Promise thereof) indicating success and the dereferenced data, or errors.
30
+ *
31
+ * @example
32
+ * // Synchronous dereference (no remote refs)
33
+ * const result = dereference({ openapi: '3.0.0', info: { title: 'My API', version: '1.0.0' } }, { sync: true });
34
+ * if (result.success) {
35
+ * console.log(result.data); // Magic proxy-wrapped document
36
+ * }
37
+ *
38
+ * @example
39
+ * // Asynchronous dereference (with remote refs)
40
+ * dereference({ $ref: 'https://example.com/api.yaml' })
41
+ * .then(result => {
42
+ * if (result.success) {
43
+ * console.log(result.data); // Fully dereferenced document
44
+ * } else {
45
+ * console.error(result.errors);
46
+ * }
47
+ * });
48
+ */
49
+ export const dereference = <Opts extends { sync?: boolean }>(
50
+ input: UnknownObject,
51
+ options?: Opts,
52
+ ): ReturnDereferenceResult<Opts> => {
53
+ if (options?.sync) {
54
+ return {
55
+ success: true,
56
+ data: createMagicProxy(input),
57
+ } as ReturnDereferenceResult<Opts>
58
+ }
59
+
60
+ const errors: string[] = []
61
+
62
+ return bundle(input, {
63
+ plugins: [fetchUrls()],
64
+ treeShake: false,
65
+ urlMap: true,
66
+ hooks: {
67
+ onResolveError(node) {
68
+ errors.push(`Failed to resolve ${node.$ref}`)
69
+ },
70
+ },
71
+ }).then((result) => {
72
+ if (errors.length > 0) {
73
+ return {
74
+ success: false,
75
+ errors,
76
+ }
77
+ }
78
+
79
+ return {
80
+ success: true,
81
+ data: createMagicProxy(result as UnknownObject),
82
+ }
83
+ }) as ReturnDereferenceResult<Opts>
84
+ }
@@ -0,0 +1,2 @@
1
+ // biome-ignore lint/performance/noBarrelFile: <explanation>
2
+ export { dereference } from './dereference'