@scalar/workspace-store 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 (198) hide show
  1. package/.turbo/turbo-build.log +10 -0
  2. package/CHANGELOG.md +13 -0
  3. package/README.md +199 -0
  4. package/dist/create-server-workspace-store.d.ts +151 -0
  5. package/dist/create-server-workspace-store.d.ts.map +1 -0
  6. package/dist/create-server-workspace-store.js +199 -0
  7. package/dist/create-server-workspace-store.js.map +7 -0
  8. package/dist/create-workspace-store.d.ts +19773 -0
  9. package/dist/create-workspace-store.d.ts.map +1 -0
  10. package/dist/create-workspace-store.js +186 -0
  11. package/dist/create-workspace-store.js.map +7 -0
  12. package/dist/helpers/general.d.ts +88 -0
  13. package/dist/helpers/general.d.ts.map +1 -0
  14. package/dist/helpers/general.js +38 -0
  15. package/dist/helpers/general.js.map +7 -0
  16. package/dist/helpers/json-path-utils.d.ts +23 -0
  17. package/dist/helpers/json-path-utils.d.ts.map +1 -0
  18. package/dist/helpers/json-path-utils.js +16 -0
  19. package/dist/helpers/json-path-utils.js.map +7 -0
  20. package/dist/helpers/proxy.d.ts +63 -0
  21. package/dist/helpers/proxy.d.ts.map +1 -0
  22. package/dist/helpers/proxy.js +100 -0
  23. package/dist/helpers/proxy.js.map +7 -0
  24. package/dist/index.d.ts +4 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +13 -0
  27. package/dist/index.js.map +7 -0
  28. package/dist/schemas/callback.d.ts +1095 -0
  29. package/dist/schemas/callback.d.ts.map +1 -0
  30. package/dist/schemas/callback.js +11 -0
  31. package/dist/schemas/callback.js.map +7 -0
  32. package/dist/schemas/components.d.ts +2461 -0
  33. package/dist/schemas/components.d.ts.map +1 -0
  34. package/dist/schemas/components.js +38 -0
  35. package/dist/schemas/components.js.map +7 -0
  36. package/dist/schemas/contact.d.ts +10 -0
  37. package/dist/schemas/contact.d.ts.map +1 -0
  38. package/dist/schemas/contact.js +13 -0
  39. package/dist/schemas/contact.js.map +7 -0
  40. package/dist/schemas/discriminator.d.ts +12 -0
  41. package/dist/schemas/discriminator.d.ts.map +1 -0
  42. package/dist/schemas/discriminator.js +11 -0
  43. package/dist/schemas/discriminator.js.map +7 -0
  44. package/dist/schemas/encoding.d.ts +23 -0
  45. package/dist/schemas/encoding.d.ts.map +1 -0
  46. package/dist/schemas/encoding.js +13 -0
  47. package/dist/schemas/encoding.js.map +7 -0
  48. package/dist/schemas/example.d.ts +16 -0
  49. package/dist/schemas/example.d.ts.map +1 -0
  50. package/dist/schemas/example.js +15 -0
  51. package/dist/schemas/example.js.map +7 -0
  52. package/dist/schemas/external-documentation.d.ts +8 -0
  53. package/dist/schemas/external-documentation.d.ts.map +1 -0
  54. package/dist/schemas/external-documentation.js +11 -0
  55. package/dist/schemas/external-documentation.js.map +7 -0
  56. package/dist/schemas/header.d.ts +18 -0
  57. package/dist/schemas/header.d.ts.map +1 -0
  58. package/dist/schemas/header.js +13 -0
  59. package/dist/schemas/header.js.map +7 -0
  60. package/dist/schemas/info.d.ts +28 -0
  61. package/dist/schemas/info.d.ts.map +1 -0
  62. package/dist/schemas/info.js +23 -0
  63. package/dist/schemas/info.js.map +7 -0
  64. package/dist/schemas/license.d.ts +10 -0
  65. package/dist/schemas/license.d.ts.map +1 -0
  66. package/dist/schemas/license.js +13 -0
  67. package/dist/schemas/license.js.map +7 -0
  68. package/dist/schemas/link.d.ts +30 -0
  69. package/dist/schemas/link.d.ts.map +1 -0
  70. package/dist/schemas/link.js +20 -0
  71. package/dist/schemas/link.js.map +7 -0
  72. package/dist/schemas/media-type.d.ts +55 -0
  73. package/dist/schemas/media-type.d.ts.map +1 -0
  74. package/dist/schemas/media-type.js +19 -0
  75. package/dist/schemas/media-type.js.map +7 -0
  76. package/dist/schemas/oauth-flow.d.ts +12 -0
  77. package/dist/schemas/oauth-flow.d.ts.map +1 -0
  78. package/dist/schemas/oauth-flow.js +15 -0
  79. package/dist/schemas/oauth-flow.js.map +7 -0
  80. package/dist/schemas/oauthflows.d.ts +34 -0
  81. package/dist/schemas/oauthflows.d.ts.map +1 -0
  82. package/dist/schemas/oauthflows.js +16 -0
  83. package/dist/schemas/oauthflows.js.map +7 -0
  84. package/dist/schemas/openapi-document.d.ts +4683 -0
  85. package/dist/schemas/openapi-document.d.ts.map +1 -0
  86. package/dist/schemas/openapi-document.js +35 -0
  87. package/dist/schemas/openapi-document.js.map +7 -0
  88. package/dist/schemas/operation-without-callback.d.ts +190 -0
  89. package/dist/schemas/operation-without-callback.d.ts.map +1 -0
  90. package/dist/schemas/operation-without-callback.js +39 -0
  91. package/dist/schemas/operation-without-callback.js.map +7 -0
  92. package/dist/schemas/parameter.d.ts +25 -0
  93. package/dist/schemas/parameter.d.ts.map +1 -0
  94. package/dist/schemas/parameter.js +22 -0
  95. package/dist/schemas/parameter.js.map +7 -0
  96. package/dist/schemas/path-item.d.ts +1106 -0
  97. package/dist/schemas/path-item.d.ts.map +1 -0
  98. package/dist/schemas/path-item.js +37 -0
  99. package/dist/schemas/path-item.js.map +7 -0
  100. package/dist/schemas/paths.d.ts +1093 -0
  101. package/dist/schemas/paths.d.ts.map +1 -0
  102. package/dist/schemas/paths.js +11 -0
  103. package/dist/schemas/paths.js.map +7 -0
  104. package/dist/schemas/reference.d.ts +17 -0
  105. package/dist/schemas/reference.d.ts.map +1 -0
  106. package/dist/schemas/reference.js +15 -0
  107. package/dist/schemas/reference.js.map +7 -0
  108. package/dist/schemas/request-body.d.ts +54 -0
  109. package/dist/schemas/request-body.d.ts.map +1 -0
  110. package/dist/schemas/request-body.js +14 -0
  111. package/dist/schemas/request-body.js.map +7 -0
  112. package/dist/schemas/response.d.ts +84 -0
  113. package/dist/schemas/response.d.ts.map +1 -0
  114. package/dist/schemas/response.js +19 -0
  115. package/dist/schemas/response.js.map +7 -0
  116. package/dist/schemas/responses.d.ts +94 -0
  117. package/dist/schemas/responses.d.ts.map +1 -0
  118. package/dist/schemas/responses.js +8 -0
  119. package/dist/schemas/responses.js.map +7 -0
  120. package/dist/schemas/schema.d.ts +34 -0
  121. package/dist/schemas/schema.d.ts.map +1 -0
  122. package/dist/schemas/schema.js +22 -0
  123. package/dist/schemas/schema.js.map +7 -0
  124. package/dist/schemas/security-requirement.d.ts +11 -0
  125. package/dist/schemas/security-requirement.d.ts.map +1 -0
  126. package/dist/schemas/security-requirement.js +10 -0
  127. package/dist/schemas/security-requirement.js.map +7 -0
  128. package/dist/schemas/security-scheme.d.ts +137 -0
  129. package/dist/schemas/security-scheme.d.ts.map +1 -0
  130. package/dist/schemas/security-scheme.js +56 -0
  131. package/dist/schemas/security-scheme.js.map +7 -0
  132. package/dist/schemas/server-variable.d.ts +10 -0
  133. package/dist/schemas/server-variable.d.ts.map +1 -0
  134. package/dist/schemas/server-variable.js +13 -0
  135. package/dist/schemas/server-variable.js.map +7 -0
  136. package/dist/schemas/server-workspace.d.ts +14043 -0
  137. package/dist/schemas/server-workspace.d.ts.map +1 -0
  138. package/dist/schemas/server-workspace.js +29 -0
  139. package/dist/schemas/server-workspace.js.map +7 -0
  140. package/dist/schemas/server.d.ts +14 -0
  141. package/dist/schemas/server.d.ts.map +1 -0
  142. package/dist/schemas/server.js +14 -0
  143. package/dist/schemas/server.js.map +7 -0
  144. package/dist/schemas/tag.d.ts +13 -0
  145. package/dist/schemas/tag.d.ts.map +1 -0
  146. package/dist/schemas/tag.js +14 -0
  147. package/dist/schemas/tag.js.map +7 -0
  148. package/dist/schemas/xml.d.ts +18 -0
  149. package/dist/schemas/xml.d.ts.map +1 -0
  150. package/dist/schemas/xml.js +17 -0
  151. package/dist/schemas/xml.js.map +7 -0
  152. package/esbuild.ts +6 -0
  153. package/package.json +54 -0
  154. package/src/create-server-workspace-store.test.ts +429 -0
  155. package/src/create-server-workspace-store.ts +339 -0
  156. package/src/create-workspace-store.test.ts +488 -0
  157. package/src/create-workspace-store.ts +282 -0
  158. package/src/helpers/general.ts +115 -0
  159. package/src/helpers/json-path-utils.test.ts +13 -0
  160. package/src/helpers/json-path-utils.ts +38 -0
  161. package/src/helpers/proxy.test.ts +61 -0
  162. package/src/helpers/proxy.ts +213 -0
  163. package/src/index.ts +8 -0
  164. package/src/schemas/callback.ts +13 -0
  165. package/src/schemas/components.ts +36 -0
  166. package/src/schemas/contact.ts +11 -0
  167. package/src/schemas/discriminator.ts +13 -0
  168. package/src/schemas/encoding.ts +17 -0
  169. package/src/schemas/example.ts +17 -0
  170. package/src/schemas/external-documentation.ts +9 -0
  171. package/src/schemas/header.ts +19 -0
  172. package/src/schemas/info.ts +23 -0
  173. package/src/schemas/license.ts +11 -0
  174. package/src/schemas/link.ts +24 -0
  175. package/src/schemas/media-type.ts +21 -0
  176. package/src/schemas/oauth-flow.ts +13 -0
  177. package/src/schemas/oauthflows.ts +16 -0
  178. package/src/schemas/openapi-document.ts +34 -0
  179. package/src/schemas/operation-without-callback.ts +37 -0
  180. package/src/schemas/parameter.ts +26 -0
  181. package/src/schemas/path-item.ts +35 -0
  182. package/src/schemas/paths.ts +11 -0
  183. package/src/schemas/reference.ts +18 -0
  184. package/src/schemas/request-body.ts +12 -0
  185. package/src/schemas/response.ts +16 -0
  186. package/src/schemas/responses.ts +14 -0
  187. package/src/schemas/schema.ts +26 -0
  188. package/src/schemas/security-requirement.ts +16 -0
  189. package/src/schemas/security-scheme.ts +58 -0
  190. package/src/schemas/server-variable.ts +11 -0
  191. package/src/schemas/server-workspace.ts +36 -0
  192. package/src/schemas/server.ts +12 -0
  193. package/src/schemas/tag.ts +12 -0
  194. package/src/schemas/xml.ts +19 -0
  195. package/test/helpers.ts +16 -0
  196. package/tsconfig.build.json +12 -0
  197. package/tsconfig.json +8 -0
  198. package/vite.config.ts +9 -0
@@ -0,0 +1,282 @@
1
+ import { reactive, toRaw } from 'vue'
2
+ import type { WorkspaceMeta, WorkspaceDocumentMeta, Workspace } from './schemas/server-workspace'
3
+ import { createMagicProxy, getRaw } from './helpers/proxy'
4
+ import { fetchUrl, isObject, readLocalFile, resolveContents } from '@/helpers/general'
5
+ import { getValueByPath, parseJsonPointer } from '@/helpers/json-path-utils'
6
+
7
+ type WorkspaceDocumentMetaInput = { meta?: WorkspaceDocumentMeta; name: string }
8
+ type WorkspaceDocumentInput =
9
+ | ({ document: Record<string, unknown> } & WorkspaceDocumentMetaInput)
10
+ | ({ url: string } & WorkspaceDocumentMetaInput)
11
+ | ({ path: string } & WorkspaceDocumentMetaInput)
12
+
13
+ /**
14
+ * Resolves a workspace document from various input sources (URL, local file, or direct document object).
15
+ *
16
+ * @param workspaceDocument - The document input to resolve, which can be:
17
+ * - A URL to fetch the document from
18
+ * - A local file path to read the document from
19
+ * - A direct document object
20
+ * @returns A promise that resolves to an object containing:
21
+ * - ok: boolean indicating if the resolution was successful
22
+ * - data: The resolved document data
23
+ *
24
+ * @example
25
+ * // Resolve from URL
26
+ * const urlDoc = await loadDocument({ name: 'api', url: 'https://api.example.com/openapi.json' })
27
+ *
28
+ * // Resolve from local file
29
+ * const fileDoc = await loadDocument({ name: 'local', path: './openapi.json' })
30
+ *
31
+ * // Resolve direct document
32
+ * const directDoc = await loadDocument({
33
+ * name: 'inline',
34
+ * document: { openapi: '3.0.0', paths: {} }
35
+ * })
36
+ */
37
+ async function loadDocument(workspaceDocument: WorkspaceDocumentInput) {
38
+ if ('url' in workspaceDocument) {
39
+ return fetchUrl(workspaceDocument.url)
40
+ }
41
+
42
+ if ('path' in workspaceDocument) {
43
+ return readLocalFile(workspaceDocument.path)
44
+ }
45
+
46
+ return {
47
+ ok: true as const,
48
+ data: workspaceDocument.document,
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Creates a reactive workspace store that manages documents and their metadata.
54
+ * The store provides functionality for accessing, updating, and resolving document references.
55
+ *
56
+ * @param workspaceProps - Configuration object for the workspace
57
+ * @param workspaceProps.meta - Optional metadata for the workspace
58
+ * @param workspaceProps.documents - Optional record of documents to initialize the workspace with
59
+ * @returns An object containing methods and getters for managing the workspace
60
+ */
61
+ export async function createWorkspaceStore(workspaceProps?: {
62
+ meta?: WorkspaceMeta
63
+ documents?: WorkspaceDocumentInput[]
64
+ }) {
65
+ // Create a reactive workspace object with proxied documents
66
+ // Each document is wrapped in a proxy to enable reactive updates and reference resolution
67
+ const workspace = reactive({
68
+ ...workspaceProps?.meta,
69
+ documents: (
70
+ await Promise.all(
71
+ (workspaceProps?.documents ?? []).map<
72
+ Promise<{ name: string; meta?: WorkspaceDocumentMeta; document: Record<string, unknown> }>
73
+ >(async (data) => {
74
+ const resolved = await loadDocument(data)
75
+
76
+ if (!resolved.ok) {
77
+ console.error(`Can not load the document '${data.name}'`)
78
+ return {
79
+ name: data.name,
80
+ meta: data.meta,
81
+ document: {},
82
+ }
83
+ }
84
+
85
+ return {
86
+ name: data.name,
87
+ meta: data.meta,
88
+ document: isObject(resolved.data) ? (resolved.data as Record<string, unknown>) : {},
89
+ }
90
+ }),
91
+ )
92
+ ).reduce<Record<string, Record<string, unknown>>>((acc, { name, meta, document }) => {
93
+ /**
94
+ * We wrap each document in the magic proxy to enable auto-resolving of references
95
+ */
96
+ acc[name] = createMagicProxy({ ...document, ...meta })
97
+ return acc
98
+ }, {}),
99
+ /**
100
+ * Returns the currently active document from the workspace.
101
+ * The active document is determined by the 'x-scalar-active-document' metadata field,
102
+ * falling back to the first document in the workspace if no active document is specified.
103
+ *
104
+ * @returns The active document or undefined if no document is found
105
+ */
106
+ get activeDocument(): (typeof workspace.documents)[number] | undefined {
107
+ const activeDocumentKey = workspace['x-scalar-active-document'] ?? Object.keys(workspace.documents)[0] ?? ''
108
+ return workspace.documents[activeDocumentKey]
109
+ },
110
+ }) as Workspace
111
+
112
+ return {
113
+ /**
114
+ * Returns the raw (non-reactive) workspace object
115
+ */
116
+ get rawWorkspace() {
117
+ return toRaw(workspace)
118
+ },
119
+ /**
120
+ * Returns the reactive workspace object with an additional activeDocument getter
121
+ */
122
+ get workspace() {
123
+ return workspace
124
+ },
125
+ /**
126
+ * Updates a specific metadata field in the workspace
127
+ * @param key - The metadata field to update
128
+ * @param value - The new value for the field
129
+ * @example
130
+ * // Update the workspace title
131
+ * update('x-scalar-active-document', 'document-name')
132
+ */
133
+ update<K extends keyof WorkspaceMeta>(key: K, value: WorkspaceMeta[K]) {
134
+ // @ts-ignore
135
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
136
+ throw new Error('Invalid key: cannot modify prototype')
137
+ }
138
+ Object.assign(workspace, { [key]: value })
139
+ },
140
+ /**
141
+ * Updates a specific metadata field in a document
142
+ * @param name - The name of the document to update ('active' or a specific document name)
143
+ * @param key - The metadata field to update
144
+ * @param value - The new value for the field
145
+ * @throws Error if the specified document doesn't exist
146
+ * @example
147
+ * // Update the auth of the active document
148
+ * updateDocument('active', 'x-scalar-active-auth', 'Bearer')
149
+ * // Update the auth of a specific document
150
+ * updateDocument('document-name', 'x-scalar-active-auth', 'Bearer')
151
+ */
152
+ updateDocument<K extends keyof WorkspaceDocumentMeta>(
153
+ name: 'active' | (string & {}),
154
+ key: K,
155
+ value: WorkspaceDocumentMeta[K],
156
+ ) {
157
+ const currentDocument =
158
+ workspace.documents[
159
+ name === 'active'
160
+ ? (workspace['x-scalar-active-document'] ?? workspaceProps?.documents?.[0].name ?? '')
161
+ : name
162
+ ]
163
+
164
+ if (!currentDocument) {
165
+ throw 'Please select a valid document'
166
+ }
167
+
168
+ Object.assign(currentDocument, { [key]: value })
169
+ },
170
+ /**
171
+ * Resolves a reference in the active document by following the provided path and resolving any external $ref references.
172
+ * This method traverses the document structure following the given path and resolves any $ref references it encounters.
173
+ * During resolution, it sets a loading status and updates the reference with the resolved content.
174
+ *
175
+ * @param path - Array of strings representing the path to the reference (e.g. ['paths', '/users', 'get', 'responses', '200'])
176
+ * @throws Error if the path is invalid or empty
177
+ * @example
178
+ * // Resolve a reference in the active document
179
+ * resolve(['paths', '/users', 'get', 'responses', '200'])
180
+ */
181
+ resolve: async (path: string[]) => {
182
+ if (path.length <= 1) {
183
+ throw 'Please provide a valid path'
184
+ }
185
+
186
+ const lastPathSegment = path.pop()! // We are sure there is at least an element on the array
187
+
188
+ const activeDocument =
189
+ workspace.documents[workspace['x-scalar-active-document'] ?? Object.keys(workspace.documents)[0] ?? '']
190
+
191
+ let parent = activeDocument as Record<string, any>
192
+
193
+ for (const p of path) {
194
+ parent = parent[p]
195
+ }
196
+
197
+ // Keep track of objects we've already processed to prevent infinite loops
198
+ const processedObjects = new WeakSet()
199
+
200
+ const resolveRecursive = async (root: unknown, targetKey: string) => {
201
+ if (!root && !isObject(root)) {
202
+ return
203
+ }
204
+
205
+ const target = (root as Record<string, unknown>)[targetKey]
206
+
207
+ if (!target || !isObject(target)) {
208
+ return
209
+ }
210
+
211
+ // Unwrap the target from the proxy
212
+ const rawTarget = getRaw(target)
213
+
214
+ // Skip if we've already processed this object
215
+ if (processedObjects.has(rawTarget)) {
216
+ return
217
+ }
218
+
219
+ // Mark this object as processed
220
+ processedObjects.add(rawTarget)
221
+
222
+ if (typeof target === 'object' && '$ref' in target && typeof target['$ref'] === 'string') {
223
+ const ref = target['$ref']
224
+
225
+ // Set the status to loading while we resolve the ref
226
+ Object.assign(target, { '$status': 'loading' })
227
+
228
+ const [path, pointer] = ref.split('#')
229
+ const result = await resolveContents(path)
230
+
231
+ if (result.ok) {
232
+ if (targetKey === '__proto__' || targetKey === 'constructor' || targetKey === 'prototype') {
233
+ throw new Error('Invalid key: cannot modify prototype')
234
+ }
235
+
236
+ Object.assign(root as object, { [targetKey]: getValueByPath(result.data, parseJsonPointer(pointer)) })
237
+
238
+ await resolveRecursive(root, targetKey)
239
+ } else {
240
+ Object.assign(target, { '$status': 'error' })
241
+ }
242
+
243
+ return
244
+ }
245
+
246
+ await Promise.all(Object.keys(target).map((key) => resolveRecursive(target, key)))
247
+ }
248
+
249
+ return resolveRecursive(parent, lastPathSegment)
250
+ },
251
+ /**
252
+ * Adds a new document to the workspace
253
+ * @param document - The document content to add. This should be a valid OpenAPI/Swagger document or other supported format
254
+ * @param meta - Metadata for the document, including its name and other properties defined in WorkspaceDocumentMeta
255
+ * @example
256
+ * // Add a new OpenAPI document to the workspace
257
+ * store.addDocument({
258
+ * name: 'name',
259
+ * document: {
260
+ * openapi: '3.0.0',
261
+ * info: { title: 'title' },
262
+ * },
263
+ * meta: {
264
+ * 'x-scalar-active-auth': 'Bearer',
265
+ * 'x-scalar-active-server': 'production'
266
+ * }
267
+ * })
268
+ */
269
+ addDocument: async (input: WorkspaceDocumentInput) => {
270
+ const { name, meta } = input
271
+
272
+ const resolve = await loadDocument(input)
273
+
274
+ if (!resolve.ok || !isObject(resolve.data)) {
275
+ console.error(`Can not load the document '${name}'`)
276
+ return
277
+ }
278
+
279
+ workspace.documents[name] = createMagicProxy({ ...(resolve.data as Record<string, unknown>), ...meta })
280
+ },
281
+ }
282
+ }
@@ -0,0 +1,115 @@
1
+ import fs from 'node:fs/promises'
2
+
3
+ export type UnknownObject = Record<string, unknown>
4
+
5
+ /**
6
+ * Returns true if the value is a non-null object (but not an array).
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * isObject({}) // true
11
+ * isObject([]) // false
12
+ * isObject(null) // false
13
+ * ```
14
+ */
15
+ export function isObject(value: unknown): value is UnknownObject {
16
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
17
+ }
18
+
19
+ /**
20
+ * Checks if a string is a remote URL (starts with http:// or https://)
21
+ * @param value - The URL string to check
22
+ * @returns true if the string is a remote URL, false otherwise
23
+ * @example
24
+ * ```ts
25
+ * isRemoteUrl('https://example.com/schema.json') // true
26
+ * isRemoteUrl('http://api.example.com/schemas/user.json') // true
27
+ * isRemoteUrl('#/components/schemas/User') // false
28
+ * isRemoteUrl('./local-schema.json') // false
29
+ * ```
30
+ */
31
+ export function isRemoteUrl(value: string): boolean {
32
+ return value.startsWith('http://') || value.startsWith('https://')
33
+ }
34
+
35
+ /**
36
+ * Checks if a string is a local reference (starts with #)
37
+ * @param value - The reference string to check
38
+ * @returns true if the string is a local reference, false otherwise
39
+ * @example
40
+ * ```ts
41
+ * isLocalRef('#/components/schemas/User') // true
42
+ * isLocalRef('https://example.com/schema.json') // false
43
+ * isLocalRef('./local-schema.json') // false
44
+ * ```
45
+ */
46
+ export function isLocalRef(value: string): boolean {
47
+ return value.startsWith('#')
48
+ }
49
+
50
+ type ResolveResult = { ok: false } | { ok: true; data: unknown }
51
+
52
+ /**
53
+ * Fetches and parses JSON data from a remote URL.
54
+ *
55
+ * @param value - The URL to fetch data from
56
+ * @returns A result object containing either the parsed JSON data or an error indicator
57
+ * @example
58
+ * ```ts
59
+ * const result = await fetchUrl('https://api.example.com/data')
60
+ * if (result.ok) {
61
+ * console.log(result.data) // The parsed JSON data
62
+ * }
63
+ * ```
64
+ */
65
+ export async function fetchUrl(value: string): Promise<ResolveResult> {
66
+ const response = await fetch(value)
67
+
68
+ if (response.ok) {
69
+ const body = await response.json()
70
+ return { ok: true, data: body }
71
+ }
72
+
73
+ return { ok: false }
74
+ }
75
+
76
+ /**
77
+ * Reads and parses a local JSON file from the filesystem.
78
+ *
79
+ * @param value - The file path to read from
80
+ * @returns A result object containing either the parsed JSON data or an error indicator
81
+ * @example
82
+ * ```ts
83
+ * const result = await readLocalFile('./data.json')
84
+ * if (result.ok) {
85
+ * console.log(result.data) // The parsed JSON data
86
+ * }
87
+ * ```
88
+ */
89
+ export async function readLocalFile(value: string): Promise<ResolveResult> {
90
+ try {
91
+ const contents = await fs.readFile(value, 'utf-8')
92
+
93
+ return { ok: true, data: JSON.parse(contents) }
94
+ } catch {
95
+ return { ok: false }
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Resolves a reference by attempting to fetch data from either a remote URL or local filesystem.
101
+ * The function automatically determines whether to use fetchUrl() for remote URLs or readLocalFile() for local paths.
102
+ *
103
+ * @param value - The reference string to resolve (URL or file path)
104
+ * @returns A result object containing either the resolved data or an error indicator
105
+ * @example
106
+ * ```ts
107
+ * const result = await resolveContents('https://api.example.com/data')
108
+ * if (result.ok) {
109
+ * console.log(result.data) // The resolved data
110
+ * }
111
+ * ```
112
+ */
113
+ export async function resolveContents(value: string): Promise<ResolveResult> {
114
+ return isRemoteUrl(value) ? await fetchUrl(value) : await readLocalFile(value)
115
+ }
@@ -0,0 +1,13 @@
1
+ import { parseJsonPointer } from '@/helpers/json-path-utils'
2
+ import { describe, expect, test } from 'vitest'
3
+
4
+ describe('parseJsonPointer', () => {
5
+ test.each([
6
+ ['#/users/name', ['users', 'name']],
7
+ ['#/', []],
8
+ ['', []],
9
+ ['users/name', ['users', 'name']],
10
+ ])('should correctly parse json pointers', (a, b) => {
11
+ expect(parseJsonPointer(a)).toEqual(b)
12
+ })
13
+ })
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Parses a JSON Pointer string into an array of path segments
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * parseJsonPointer('#/components/schemas/User')
7
+ *
8
+ * ['components', 'schemas', 'User']
9
+ * ```
10
+ */
11
+ export function parseJsonPointer(pointer: string): string[] {
12
+ return (
13
+ pointer
14
+ // Split on '/'
15
+ .split('/')
16
+ // Remove the leading '#' if present
17
+ .filter((segment, index) => (index !== 0 || segment !== '#') && segment)
18
+ )
19
+ }
20
+
21
+ /**
22
+ * Retrieves a nested value from the source document using a path array
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * getValueByPath(document, ['components', 'schemas', 'User'])
27
+ *
28
+ * { id: '123', name: 'John Doe' }
29
+ * ```
30
+ */
31
+ export function getValueByPath(obj: any, pointer: string[]): unknown {
32
+ return pointer.reduce((acc, part) => {
33
+ if (acc === undefined || acc === null) {
34
+ return undefined
35
+ }
36
+ return acc[part]
37
+ }, obj)
38
+ }
@@ -0,0 +1,61 @@
1
+ import { createMagicProxy } from '@/helpers/proxy'
2
+ import { describe, expect, test } from 'vitest'
3
+
4
+ describe('createMagicProxy', () => {
5
+ test('should correctly proxy internal refs', () => {
6
+ const input = {
7
+ a: 'hello',
8
+ b: {
9
+ '$ref': '#/a',
10
+ },
11
+ }
12
+
13
+ const result = createMagicProxy(input)
14
+
15
+ expect(result.b).toBe('hello')
16
+ })
17
+
18
+ test('should correctly proxy deep nested refs', () => {
19
+ const input = {
20
+ a: {
21
+ b: {
22
+ c: {
23
+ d: {
24
+ prop: 'hello',
25
+ },
26
+ e: {
27
+ '$ref': '#/a/b/c/d',
28
+ },
29
+ },
30
+ },
31
+ },
32
+ }
33
+
34
+ const result = createMagicProxy(input) as any
35
+ expect(result.a.b.c.e.prop).toBe('hello')
36
+ })
37
+
38
+ test('should correctly proxy multi refs', () => {
39
+ const input = {
40
+ a: {
41
+ b: {
42
+ c: {
43
+ prop: 'hello',
44
+ },
45
+ },
46
+ },
47
+ e: {
48
+ f: {
49
+ $ref: '#/a/b/c/prop',
50
+ },
51
+ },
52
+ d: {
53
+ $ref: '#/e/f',
54
+ },
55
+ }
56
+
57
+ const result = createMagicProxy(input)
58
+
59
+ expect(result.d).toBe('hello')
60
+ })
61
+ })