@furystack/rest 8.0.42 → 8.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 (54) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +37 -1
  3. package/esm/api-endpoint-schema.d.ts +47 -2
  4. package/esm/api-endpoint-schema.d.ts.map +1 -1
  5. package/esm/index.d.ts +4 -1
  6. package/esm/index.d.ts.map +1 -1
  7. package/esm/index.js +4 -1
  8. package/esm/index.js.map +1 -1
  9. package/esm/openapi-document.d.ts +303 -0
  10. package/esm/openapi-document.d.ts.map +1 -0
  11. package/esm/openapi-document.js +2 -0
  12. package/esm/openapi-document.js.map +1 -0
  13. package/esm/openapi-resolve-refs.d.ts +20 -0
  14. package/esm/openapi-resolve-refs.d.ts.map +1 -0
  15. package/esm/openapi-resolve-refs.js +68 -0
  16. package/esm/openapi-resolve-refs.js.map +1 -0
  17. package/esm/openapi-resolve-refs.spec.d.ts +2 -0
  18. package/esm/openapi-resolve-refs.spec.d.ts.map +1 -0
  19. package/esm/openapi-resolve-refs.spec.js +294 -0
  20. package/esm/openapi-resolve-refs.spec.js.map +1 -0
  21. package/esm/openapi-to-rest-api.d.ts +197 -0
  22. package/esm/openapi-to-rest-api.d.ts.map +1 -0
  23. package/esm/openapi-to-rest-api.js +2 -0
  24. package/esm/openapi-to-rest-api.js.map +1 -0
  25. package/esm/openapi-to-rest-api.spec.d.ts +2 -0
  26. package/esm/openapi-to-rest-api.spec.d.ts.map +1 -0
  27. package/esm/openapi-to-rest-api.spec.js +665 -0
  28. package/esm/openapi-to-rest-api.spec.js.map +1 -0
  29. package/esm/openapi-to-schema.d.ts +24 -0
  30. package/esm/openapi-to-schema.d.ts.map +1 -0
  31. package/esm/openapi-to-schema.js +145 -0
  32. package/esm/openapi-to-schema.js.map +1 -0
  33. package/esm/openapi-to-schema.spec.d.ts +2 -0
  34. package/esm/openapi-to-schema.spec.d.ts.map +1 -0
  35. package/esm/openapi-to-schema.spec.js +610 -0
  36. package/esm/openapi-to-schema.spec.js.map +1 -0
  37. package/esm/rest-api.d.ts +21 -4
  38. package/esm/rest-api.d.ts.map +1 -1
  39. package/esm/swagger-document.d.ts +2 -195
  40. package/esm/swagger-document.d.ts.map +1 -1
  41. package/esm/swagger-document.js +2 -1
  42. package/esm/swagger-document.js.map +1 -1
  43. package/package.json +3 -3
  44. package/src/api-endpoint-schema.ts +56 -3
  45. package/src/index.ts +4 -1
  46. package/src/openapi-document.ts +328 -0
  47. package/src/openapi-resolve-refs.spec.ts +324 -0
  48. package/src/openapi-resolve-refs.ts +71 -0
  49. package/src/openapi-to-rest-api.spec.ts +823 -0
  50. package/src/openapi-to-rest-api.ts +263 -0
  51. package/src/openapi-to-schema.spec.ts +707 -0
  52. package/src/openapi-to-schema.ts +163 -0
  53. package/src/rest-api.ts +26 -5
  54. package/src/swagger-document.ts +2 -220
@@ -0,0 +1,328 @@
1
+ /**
2
+ * Represents an OpenAPI 3.1 document.
3
+ * @see https://spec.openapis.org/oas/v3.1.0#openapi-object
4
+ */
5
+ export type OpenApiDocument = {
6
+ openapi: string
7
+ info: InfoObject
8
+ jsonSchemaDialect?: string
9
+ externalDocs?: ExternalDocumentationObject
10
+ servers?: ServerObject[]
11
+ tags?: TagObject[]
12
+ security?: SecurityRequirementObject[]
13
+ paths?: Record<string, PathItem>
14
+ webhooks?: Record<string, PathItem>
15
+ components?: ComponentsObject
16
+ [key: `x-${string}`]: unknown
17
+ }
18
+
19
+ /** @deprecated Use OpenApiDocument instead */
20
+ export type SwaggerDocument = OpenApiDocument
21
+
22
+ /**
23
+ * Metadata about the API.
24
+ * @see https://spec.openapis.org/oas/v3.1.0#info-object
25
+ */
26
+ export type InfoObject = {
27
+ title: string
28
+ version: string
29
+ description?: string
30
+ summary?: string
31
+ termsOfService?: string
32
+ contact?: ContactObject
33
+ license?: LicenseObject
34
+ [key: `x-${string}`]: unknown
35
+ }
36
+
37
+ /**
38
+ * Contact information for the API.
39
+ * @see https://spec.openapis.org/oas/v3.1.0#contact-object
40
+ */
41
+ export type ContactObject = {
42
+ name?: string
43
+ url?: string
44
+ email?: string
45
+ [key: `x-${string}`]: unknown
46
+ }
47
+
48
+ /**
49
+ * License information for the API.
50
+ * @see https://spec.openapis.org/oas/v3.1.0#license-object
51
+ */
52
+ export type LicenseObject = {
53
+ name: string
54
+ identifier?: string
55
+ url?: string
56
+ [key: `x-${string}`]: unknown
57
+ }
58
+
59
+ /**
60
+ * A reference to external documentation.
61
+ * @see https://spec.openapis.org/oas/v3.1.0#external-documentation-object
62
+ */
63
+ export type ExternalDocumentationObject = {
64
+ url: string
65
+ description?: string
66
+ [key: `x-${string}`]: unknown
67
+ }
68
+
69
+ /**
70
+ * An object representing a server.
71
+ * @see https://spec.openapis.org/oas/v3.1.0#server-object
72
+ */
73
+ export type ServerObject = {
74
+ url: string
75
+ description?: string
76
+ variables?: Record<string, ServerVariableObject>
77
+ [key: `x-${string}`]: unknown
78
+ }
79
+
80
+ /**
81
+ * An object representing a server variable for server URL template substitution.
82
+ * @see https://spec.openapis.org/oas/v3.1.0#server-variable-object
83
+ */
84
+ export type ServerVariableObject = {
85
+ default: string
86
+ description?: string
87
+ enum?: string[]
88
+ [key: `x-${string}`]: unknown
89
+ }
90
+
91
+ /**
92
+ * Adds metadata to a single tag used by operations.
93
+ * @see https://spec.openapis.org/oas/v3.1.0#tag-object
94
+ */
95
+ export type TagObject = {
96
+ name: string
97
+ description?: string
98
+ externalDocs?: ExternalDocumentationObject
99
+ [key: `x-${string}`]: unknown
100
+ }
101
+
102
+ /**
103
+ * Lists the required security schemes to execute an operation.
104
+ * Each entry maps a security scheme name to a list of required scopes.
105
+ * @see https://spec.openapis.org/oas/v3.1.0#security-requirement-object
106
+ */
107
+ export type SecurityRequirementObject = Record<string, string[]>
108
+
109
+ /**
110
+ * Holds a set of reusable objects for the OpenAPI document.
111
+ * @see https://spec.openapis.org/oas/v3.1.0#components-object
112
+ */
113
+ export type ComponentsObject = {
114
+ schemas?: Record<string, object | boolean>
115
+ responses?: Record<string, ResponseObject | ReferenceObject>
116
+ parameters?: Record<string, ParameterObject | ReferenceObject>
117
+ examples?: Record<string, ExampleObject | ReferenceObject>
118
+ requestBodies?: Record<string, RequestBodyObject | ReferenceObject>
119
+ headers?: Record<string, HeaderObject | ReferenceObject>
120
+ securitySchemes?: Record<string, SecuritySchemeObject | ReferenceObject>
121
+ links?: Record<string, LinkObject | ReferenceObject>
122
+ callbacks?: Record<string, CallbackObject | ReferenceObject>
123
+ pathItems?: Record<string, PathItem | ReferenceObject>
124
+ [key: `x-${string}`]: unknown
125
+ }
126
+
127
+ /**
128
+ * A JSON Reference object pointing to another location in the document or an external resource.
129
+ * @see https://spec.openapis.org/oas/v3.1.0#reference-object
130
+ */
131
+ export type ReferenceObject = {
132
+ $ref: string
133
+ description?: string
134
+ summary?: string
135
+ }
136
+
137
+ /**
138
+ * Describes the operations available on a single path.
139
+ * @see https://spec.openapis.org/oas/v3.1.0#path-item-object
140
+ */
141
+ export type PathItem = {
142
+ summary?: string
143
+ description?: string
144
+ get?: Operation
145
+ put?: Operation
146
+ post?: Operation
147
+ delete?: Operation
148
+ options?: Operation
149
+ head?: Operation
150
+ patch?: Operation
151
+ trace?: Operation
152
+ servers?: ServerObject[]
153
+ parameters?: Array<ParameterObject | ReferenceObject>
154
+ [key: `x-${string}`]: unknown
155
+ }
156
+
157
+ /**
158
+ * Describes a single API operation on a path.
159
+ * @see https://spec.openapis.org/oas/v3.1.0#operation-object
160
+ */
161
+ export type Operation = {
162
+ tags?: string[]
163
+ summary?: string
164
+ description?: string
165
+ externalDocs?: ExternalDocumentationObject
166
+ operationId?: string
167
+ parameters?: Array<ParameterObject | ReferenceObject>
168
+ requestBody?: RequestBodyObject | ReferenceObject
169
+ responses: ResponsesObject
170
+ callbacks?: Record<string, CallbackObject | ReferenceObject>
171
+ deprecated?: boolean
172
+ security?: SecurityRequirementObject[]
173
+ servers?: ServerObject[]
174
+ [key: `x-${string}`]: unknown
175
+ }
176
+
177
+ /**
178
+ * Describes a single operation parameter (path, query, header, or cookie).
179
+ * @see https://spec.openapis.org/oas/v3.1.0#parameter-object
180
+ */
181
+ export type ParameterObject = {
182
+ name: string
183
+ in: 'query' | 'header' | 'path' | 'cookie'
184
+ description?: string
185
+ required?: boolean
186
+ deprecated?: boolean
187
+ allowEmptyValue?: boolean
188
+ style?: string
189
+ explode?: boolean
190
+ allowReserved?: boolean
191
+ schema?: object | boolean
192
+ example?: unknown
193
+ examples?: Record<string, ExampleObject | ReferenceObject>
194
+ content?: Record<string, MediaTypeObject>
195
+ [key: `x-${string}`]: unknown
196
+ }
197
+
198
+ /**
199
+ * Describes the request body of an operation.
200
+ * @see https://spec.openapis.org/oas/v3.1.0#request-body-object
201
+ */
202
+ export type RequestBodyObject = {
203
+ description?: string
204
+ content: Record<string, MediaTypeObject>
205
+ required?: boolean
206
+ [key: `x-${string}`]: unknown
207
+ }
208
+
209
+ /**
210
+ * Describes the content of a request body or response for a specific media type.
211
+ * @see https://spec.openapis.org/oas/v3.1.0#media-type-object
212
+ */
213
+ export type MediaTypeObject = {
214
+ schema?: object | boolean
215
+ example?: unknown
216
+ examples?: Record<string, ExampleObject | ReferenceObject>
217
+ encoding?: Record<string, EncodingObject>
218
+ [key: `x-${string}`]: unknown
219
+ }
220
+
221
+ /**
222
+ * Describes the encoding properties for a specific property in a request body.
223
+ * @see https://spec.openapis.org/oas/v3.1.0#encoding-object
224
+ */
225
+ export type EncodingObject = {
226
+ contentType?: string
227
+ headers?: Record<string, HeaderObject | ReferenceObject>
228
+ style?: string
229
+ explode?: boolean
230
+ allowReserved?: boolean
231
+ [key: `x-${string}`]: unknown
232
+ }
233
+
234
+ /**
235
+ * A map of HTTP status codes to response objects describing the operation responses.
236
+ * @see https://spec.openapis.org/oas/v3.1.0#responses-object
237
+ */
238
+ export type ResponsesObject = Record<string, ResponseObject | ReferenceObject>
239
+
240
+ /**
241
+ * Describes a single response from an API operation.
242
+ * @see https://spec.openapis.org/oas/v3.1.0#response-object
243
+ */
244
+ export type ResponseObject = {
245
+ description: string
246
+ headers?: Record<string, HeaderObject | ReferenceObject>
247
+ content?: Record<string, MediaTypeObject>
248
+ links?: Record<string, LinkObject | ReferenceObject>
249
+ [key: `x-${string}`]: unknown
250
+ }
251
+
252
+ /**
253
+ * Describes a header parameter, equivalent to a ParameterObject without `name` and `in`.
254
+ * @see https://spec.openapis.org/oas/v3.1.0#header-object
255
+ */
256
+ export type HeaderObject = Omit<ParameterObject, 'name' | 'in'>
257
+
258
+ /**
259
+ * An object holding a reusable example value.
260
+ * @see https://spec.openapis.org/oas/v3.1.0#example-object
261
+ */
262
+ export type ExampleObject = {
263
+ summary?: string
264
+ description?: string
265
+ value?: unknown
266
+ externalValue?: string
267
+ [key: `x-${string}`]: unknown
268
+ }
269
+
270
+ /**
271
+ * Represents a possible design-time link for a response.
272
+ * @see https://spec.openapis.org/oas/v3.1.0#link-object
273
+ */
274
+ export type LinkObject = {
275
+ operationRef?: string
276
+ operationId?: string
277
+ parameters?: Record<string, unknown>
278
+ requestBody?: unknown
279
+ description?: string
280
+ server?: ServerObject
281
+ [key: `x-${string}`]: unknown
282
+ }
283
+
284
+ /**
285
+ * A map of callback objects keyed by expression.
286
+ * @see https://spec.openapis.org/oas/v3.1.0#callback-object
287
+ */
288
+ export type CallbackObject = Record<string, PathItem>
289
+
290
+ /**
291
+ * Defines a security scheme for the API (apiKey, http, oauth2, openIdConnect, or mutualTLS).
292
+ * @see https://spec.openapis.org/oas/v3.1.0#security-scheme-object
293
+ */
294
+ export type SecuritySchemeObject = {
295
+ type: 'apiKey' | 'http' | 'mutualTLS' | 'oauth2' | 'openIdConnect'
296
+ description?: string
297
+ name?: string
298
+ in?: 'query' | 'header' | 'cookie'
299
+ scheme?: string
300
+ bearerFormat?: string
301
+ flows?: OAuthFlowsObject
302
+ openIdConnectUrl?: string
303
+ [key: `x-${string}`]: unknown
304
+ }
305
+
306
+ /**
307
+ * Allows configuration of the supported OAuth flows.
308
+ * @see https://spec.openapis.org/oas/v3.1.0#oauth-flows-object
309
+ */
310
+ export type OAuthFlowsObject = {
311
+ implicit?: OAuthFlowObject
312
+ password?: OAuthFlowObject
313
+ clientCredentials?: OAuthFlowObject
314
+ authorizationCode?: OAuthFlowObject
315
+ [key: `x-${string}`]: unknown
316
+ }
317
+
318
+ /**
319
+ * Configuration details for a specific OAuth flow type.
320
+ * @see https://spec.openapis.org/oas/v3.1.0#oauth-flow-object
321
+ */
322
+ export type OAuthFlowObject = {
323
+ authorizationUrl?: string
324
+ tokenUrl?: string
325
+ refreshUrl?: string
326
+ scopes: Record<string, string>
327
+ [key: `x-${string}`]: unknown
328
+ }
@@ -0,0 +1,324 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { OpenApiDocument } from './openapi-document.js'
3
+ import { resolveOpenApiRefs } from './openapi-resolve-refs.js'
4
+
5
+ describe('resolveOpenApiRefs', () => {
6
+ describe('Schema $ref resolution', () => {
7
+ it('Should resolve $ref to components/schemas', () => {
8
+ const doc: OpenApiDocument = {
9
+ openapi: '3.1.0',
10
+ info: { title: 'Test', version: '1.0.0' },
11
+ paths: {
12
+ '/users': {
13
+ get: {
14
+ responses: {
15
+ '200': {
16
+ description: 'OK',
17
+ content: {
18
+ 'application/json': {
19
+ schema: { $ref: '#/components/schemas/User' },
20
+ },
21
+ },
22
+ },
23
+ },
24
+ },
25
+ },
26
+ },
27
+ components: {
28
+ schemas: {
29
+ User: { type: 'object', properties: { id: { type: 'string' }, name: { type: 'string' } } },
30
+ },
31
+ },
32
+ }
33
+
34
+ const resolved = resolveOpenApiRefs(doc)
35
+ const { schema } = (
36
+ resolved.paths?.['/users']?.get?.responses?.['200'] as { content: Record<string, { schema: unknown }> }
37
+ ).content['application/json']
38
+ expect(schema).toEqual({ type: 'object', properties: { id: { type: 'string' }, name: { type: 'string' } } })
39
+ })
40
+
41
+ it('Should resolve nested $ref chains', () => {
42
+ const doc: OpenApiDocument = {
43
+ openapi: '3.1.0',
44
+ info: { title: 'Test', version: '1.0.0' },
45
+ paths: {
46
+ '/items': {
47
+ get: {
48
+ responses: {
49
+ '200': {
50
+ description: 'OK',
51
+ content: {
52
+ 'application/json': {
53
+ schema: {
54
+ type: 'object',
55
+ properties: {
56
+ category: { $ref: '#/components/schemas/Category' },
57
+ },
58
+ },
59
+ },
60
+ },
61
+ },
62
+ },
63
+ },
64
+ },
65
+ },
66
+ components: {
67
+ schemas: {
68
+ Category: { type: 'object', properties: { id: { type: 'integer' }, name: { type: 'string' } } },
69
+ },
70
+ },
71
+ }
72
+
73
+ const resolved = resolveOpenApiRefs(doc)
74
+ const schema = (
75
+ resolved.paths?.['/items']?.get?.responses?.['200'] as { content: Record<string, { schema: unknown }> }
76
+ ).content['application/json'].schema as Record<string, unknown>
77
+ const props = (schema.properties as Record<string, unknown>).category as Record<string, unknown>
78
+ expect(props.type).toBe('object')
79
+ expect(props.properties).toEqual({ id: { type: 'integer' }, name: { type: 'string' } })
80
+ })
81
+ })
82
+
83
+ describe('Parameter $ref resolution', () => {
84
+ it('Should resolve $ref parameters', () => {
85
+ const doc: OpenApiDocument = {
86
+ openapi: '3.1.0',
87
+ info: { title: 'Test', version: '1.0.0' },
88
+ paths: {
89
+ '/items': {
90
+ get: {
91
+ parameters: [{ $ref: '#/components/parameters/LimitParam' }],
92
+ responses: { '200': { description: 'OK' } },
93
+ },
94
+ },
95
+ },
96
+ components: {
97
+ parameters: {
98
+ LimitParam: { name: 'limit', in: 'query', schema: { type: 'integer' } },
99
+ },
100
+ },
101
+ }
102
+
103
+ const resolved = resolveOpenApiRefs(doc)
104
+ const params = resolved.paths?.['/items']?.get?.parameters as Array<Record<string, unknown>>
105
+ expect(params[0].name).toBe('limit')
106
+ expect(params[0].in).toBe('query')
107
+ })
108
+ })
109
+
110
+ describe('Response $ref resolution', () => {
111
+ it('Should resolve $ref responses', () => {
112
+ const doc: OpenApiDocument = {
113
+ openapi: '3.1.0',
114
+ info: { title: 'Test', version: '1.0.0' },
115
+ paths: {
116
+ '/items': {
117
+ get: {
118
+ responses: {
119
+ '400': { $ref: '#/components/responses/BadRequest' },
120
+ },
121
+ },
122
+ },
123
+ },
124
+ components: {
125
+ responses: {
126
+ BadRequest: {
127
+ description: 'Bad request',
128
+ content: {
129
+ 'application/json': {
130
+ schema: { type: 'object', properties: { message: { type: 'string' } } },
131
+ },
132
+ },
133
+ },
134
+ },
135
+ },
136
+ }
137
+
138
+ const resolved = resolveOpenApiRefs(doc)
139
+ const resp = resolved.paths?.['/items']?.get?.responses?.['400'] as Record<string, unknown>
140
+ expect(resp.description).toBe('Bad request')
141
+ })
142
+ })
143
+
144
+ describe('RequestBody $ref resolution', () => {
145
+ it('Should resolve $ref in requestBody schema', () => {
146
+ const doc: OpenApiDocument = {
147
+ openapi: '3.1.0',
148
+ info: { title: 'Test', version: '1.0.0' },
149
+ paths: {
150
+ '/items': {
151
+ post: {
152
+ requestBody: {
153
+ content: {
154
+ 'application/json': {
155
+ schema: { $ref: '#/components/schemas/Item' },
156
+ },
157
+ },
158
+ },
159
+ responses: { '201': { description: 'Created' } },
160
+ },
161
+ },
162
+ },
163
+ components: {
164
+ schemas: {
165
+ Item: { type: 'object', properties: { name: { type: 'string' } } },
166
+ },
167
+ },
168
+ }
169
+
170
+ const resolved = resolveOpenApiRefs(doc)
171
+ const body = resolved.paths?.['/items']?.post?.requestBody as Record<string, unknown>
172
+ const schema = ((body.content as Record<string, unknown>)['application/json'] as Record<string, unknown>)
173
+ .schema as Record<string, unknown>
174
+ expect(schema.type).toBe('object')
175
+ })
176
+ })
177
+
178
+ describe('Edge cases', () => {
179
+ it('Should handle circular $ref by breaking the cycle with empty object', () => {
180
+ const doc: OpenApiDocument = {
181
+ openapi: '3.1.0',
182
+ info: { title: 'Test', version: '1.0.0' },
183
+ components: {
184
+ schemas: {
185
+ Node: {
186
+ type: 'object',
187
+ properties: {
188
+ value: { type: 'string' },
189
+ child: { $ref: '#/components/schemas/Node' },
190
+ },
191
+ },
192
+ },
193
+ },
194
+ }
195
+
196
+ const resolved = resolveOpenApiRefs(doc)
197
+ const nodeSchema = resolved.components?.schemas?.Node as Record<string, unknown>
198
+ expect(nodeSchema.type).toBe('object')
199
+ const props = nodeSchema.properties as Record<string, Record<string, unknown>>
200
+ expect(props.value).toEqual({ type: 'string' })
201
+ // The circular child ref is resolved, but the nested self-ref within it breaks the cycle
202
+ expect(props.child.type).toBe('object')
203
+ const childProps = props.child.properties as Record<string, Record<string, unknown>>
204
+ expect(childProps.value).toEqual({ type: 'string' })
205
+ expect(childProps.child).toEqual({})
206
+ })
207
+
208
+ it('Should not modify the original document', () => {
209
+ const doc: OpenApiDocument = {
210
+ openapi: '3.1.0',
211
+ info: { title: 'Test', version: '1.0.0' },
212
+ paths: {
213
+ '/items': {
214
+ get: {
215
+ responses: {
216
+ '200': {
217
+ description: 'OK',
218
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/Item' } } },
219
+ },
220
+ },
221
+ },
222
+ },
223
+ },
224
+ components: { schemas: { Item: { type: 'object' } } },
225
+ }
226
+
227
+ resolveOpenApiRefs(doc)
228
+ const schema = doc.paths?.['/items']?.get?.responses?.['200'] as Record<string, unknown>
229
+ expect((schema.content as Record<string, { schema: unknown }>)['application/json'].schema).toEqual({
230
+ $ref: '#/components/schemas/Item',
231
+ })
232
+ })
233
+
234
+ it('Should leave external $ref as-is', () => {
235
+ const doc: OpenApiDocument = {
236
+ openapi: '3.1.0',
237
+ info: { title: 'Test', version: '1.0.0' },
238
+ paths: {
239
+ '/items': {
240
+ get: {
241
+ responses: {
242
+ '200': {
243
+ description: 'OK',
244
+ content: { 'application/json': { schema: { $ref: 'external.json#/Schema' } } },
245
+ },
246
+ },
247
+ },
248
+ },
249
+ },
250
+ }
251
+
252
+ const resolved = resolveOpenApiRefs(doc)
253
+ const { schema } = (
254
+ resolved.paths?.['/items']?.get?.responses?.['200'] as { content: Record<string, { schema: unknown }> }
255
+ ).content['application/json']
256
+ expect(schema).toEqual({ $ref: 'external.json#/Schema' })
257
+ })
258
+
259
+ it('Should leave unresolvable $ref as-is', () => {
260
+ const doc: OpenApiDocument = {
261
+ openapi: '3.1.0',
262
+ info: { title: 'Test', version: '1.0.0' },
263
+ paths: {
264
+ '/items': {
265
+ get: {
266
+ responses: {
267
+ '200': {
268
+ description: 'OK',
269
+ content: { 'application/json': { schema: { $ref: '#/components/schemas/Missing' } } },
270
+ },
271
+ },
272
+ },
273
+ },
274
+ },
275
+ }
276
+
277
+ const resolved = resolveOpenApiRefs(doc)
278
+ const { schema } = (
279
+ resolved.paths?.['/items']?.get?.responses?.['200'] as { content: Record<string, { schema: unknown }> }
280
+ ).content['application/json']
281
+ expect(schema).toEqual({ $ref: '#/components/schemas/Missing' })
282
+ })
283
+
284
+ it('Should handle documents with no $ref', () => {
285
+ const doc: OpenApiDocument = {
286
+ openapi: '3.1.0',
287
+ info: { title: 'Test', version: '1.0.0' },
288
+ paths: {
289
+ '/health': { get: { responses: { '200': { description: 'OK' } } } },
290
+ },
291
+ }
292
+
293
+ const resolved = resolveOpenApiRefs(doc)
294
+ expect(resolved.paths?.['/health']?.get?.responses?.['200']).toEqual({ description: 'OK' })
295
+ })
296
+
297
+ it('Should resolve arrays of items with $ref', () => {
298
+ const doc: OpenApiDocument = {
299
+ openapi: '3.1.0',
300
+ info: { title: 'Test', version: '1.0.0' },
301
+ paths: {
302
+ '/items': {
303
+ get: {
304
+ parameters: [{ $ref: '#/components/parameters/A' }, { $ref: '#/components/parameters/B' }],
305
+ responses: { '200': { description: 'OK' } },
306
+ },
307
+ },
308
+ },
309
+ components: {
310
+ parameters: {
311
+ A: { name: 'a', in: 'query', schema: { type: 'string' } },
312
+ B: { name: 'b', in: 'query', schema: { type: 'integer' } },
313
+ },
314
+ },
315
+ }
316
+
317
+ const resolved = resolveOpenApiRefs(doc)
318
+ const params = resolved.paths?.['/items']?.get?.parameters as Array<Record<string, unknown>>
319
+ expect(params).toHaveLength(2)
320
+ expect(params[0].name).toBe('a')
321
+ expect(params[1].name).toBe('b')
322
+ })
323
+ })
324
+ })