@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.
- package/CHANGELOG.md +71 -0
- package/README.md +37 -1
- package/esm/api-endpoint-schema.d.ts +47 -2
- package/esm/api-endpoint-schema.d.ts.map +1 -1
- package/esm/index.d.ts +4 -1
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +4 -1
- package/esm/index.js.map +1 -1
- package/esm/openapi-document.d.ts +303 -0
- package/esm/openapi-document.d.ts.map +1 -0
- package/esm/openapi-document.js +2 -0
- package/esm/openapi-document.js.map +1 -0
- package/esm/openapi-resolve-refs.d.ts +20 -0
- package/esm/openapi-resolve-refs.d.ts.map +1 -0
- package/esm/openapi-resolve-refs.js +68 -0
- package/esm/openapi-resolve-refs.js.map +1 -0
- package/esm/openapi-resolve-refs.spec.d.ts +2 -0
- package/esm/openapi-resolve-refs.spec.d.ts.map +1 -0
- package/esm/openapi-resolve-refs.spec.js +294 -0
- package/esm/openapi-resolve-refs.spec.js.map +1 -0
- package/esm/openapi-to-rest-api.d.ts +197 -0
- package/esm/openapi-to-rest-api.d.ts.map +1 -0
- package/esm/openapi-to-rest-api.js +2 -0
- package/esm/openapi-to-rest-api.js.map +1 -0
- package/esm/openapi-to-rest-api.spec.d.ts +2 -0
- package/esm/openapi-to-rest-api.spec.d.ts.map +1 -0
- package/esm/openapi-to-rest-api.spec.js +665 -0
- package/esm/openapi-to-rest-api.spec.js.map +1 -0
- package/esm/openapi-to-schema.d.ts +24 -0
- package/esm/openapi-to-schema.d.ts.map +1 -0
- package/esm/openapi-to-schema.js +145 -0
- package/esm/openapi-to-schema.js.map +1 -0
- package/esm/openapi-to-schema.spec.d.ts +2 -0
- package/esm/openapi-to-schema.spec.d.ts.map +1 -0
- package/esm/openapi-to-schema.spec.js +610 -0
- package/esm/openapi-to-schema.spec.js.map +1 -0
- package/esm/rest-api.d.ts +21 -4
- package/esm/rest-api.d.ts.map +1 -1
- package/esm/swagger-document.d.ts +2 -195
- package/esm/swagger-document.d.ts.map +1 -1
- package/esm/swagger-document.js +2 -1
- package/esm/swagger-document.js.map +1 -1
- package/package.json +3 -3
- package/src/api-endpoint-schema.ts +56 -3
- package/src/index.ts +4 -1
- package/src/openapi-document.ts +328 -0
- package/src/openapi-resolve-refs.spec.ts +324 -0
- package/src/openapi-resolve-refs.ts +71 -0
- package/src/openapi-to-rest-api.spec.ts +823 -0
- package/src/openapi-to-rest-api.ts +263 -0
- package/src/openapi-to-schema.spec.ts +707 -0
- package/src/openapi-to-schema.ts +163 -0
- package/src/rest-api.ts +26 -5
- 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
|
+
})
|