@kubb/adapter-oas 5.0.0-beta.6 → 5.0.0-beta.60

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.
@@ -1,29 +1,22 @@
1
1
  import { ast } from '@kubb/core'
2
+ import type { SchemaNodeByType } from '@kubb/ast'
2
3
 
3
- type DiscriminatorTarget = {
4
+ export type DiscriminatorTarget = {
4
5
  propertyName: string
5
6
  enumValues: Array<string | number | boolean>
6
7
  }
7
8
 
8
9
  /**
9
- * Injects discriminator enum values into child schemas so they know which value identifies them.
10
+ * Builds a map of child schema names discriminator patch data by scanning the given
11
+ * top-level AST schema nodes for union schemas that carry a `discriminatorPropertyName`.
10
12
  *
11
- * Finds every union schema in `input.schemas` that has a `discriminatorPropertyName`, collects the
12
- * enum value each union member is mapped to, then adds (or replaces) that property on the matching
13
- * child object schema.
14
- *
15
- * Returns a new `InputNode` — the original is never mutated.
16
- *
17
- * @example
18
- * ```ts
19
- * const { root } = parseOas(document, options)
20
- * const next = applyDiscriminatorInheritance(root)
21
- * ```
13
+ * The streaming path calls this on a small pre-parsed subset of schemas (only the
14
+ * discriminator parents) rather than on all schemas at once.
22
15
  */
23
- export function applyDiscriminatorInheritance(root: ast.InputNode): ast.InputNode {
16
+ export function buildDiscriminatorChildMap(schemas: Array<ast.SchemaNode>): Map<string, DiscriminatorTarget> {
24
17
  const childMap = new Map<string, DiscriminatorTarget>()
25
18
 
26
- for (const schema of root.schemas) {
19
+ for (const schema of schemas) {
27
20
  // Case 1: top-level schema is a union (oneOf/anyOf with discriminator)
28
21
  // Case 2: top-level schema is an intersection wrapping a union (oneOf/anyOf + shared properties)
29
22
  let unionNode = ast.narrowSchema(schema, 'union')
@@ -50,8 +43,8 @@ export function applyDiscriminatorInheritance(root: ast.InputNode): ast.InputNod
50
43
  const intersectionNode = ast.narrowSchema(member, 'intersection')
51
44
  if (!intersectionNode?.members) continue
52
45
 
53
- let refNode: ReturnType<typeof ast.narrowSchema<'ref'>> | undefined
54
- let objNode: ReturnType<typeof ast.narrowSchema<'object'>> | undefined
46
+ let refNode: SchemaNodeByType['ref'] | null = null
47
+ let objNode: SchemaNodeByType['object'] | null = null
55
48
 
56
49
  for (const m of intersectionNode.members) {
57
50
  refNode ??= ast.narrowSchema(m, 'ref')
@@ -61,48 +54,79 @@ export function applyDiscriminatorInheritance(root: ast.InputNode): ast.InputNod
61
54
  if (!refNode?.name || !objNode) continue
62
55
 
63
56
  const prop = objNode.properties.find((p) => p.name === discriminatorPropertyName)
64
- const enumNode = prop ? ast.narrowSchema(prop.schema, 'enum') : undefined
57
+ const enumNode = prop ? ast.narrowSchema(prop.schema, 'enum') : null
65
58
  if (!enumNode?.enumValues?.length) continue
66
59
 
67
60
  const enumValues = enumNode.enumValues.filter((v): v is string | number | boolean => v !== null)
68
61
  if (!enumValues.length) continue
69
62
 
70
63
  const existing = childMap.get(refNode.name)
71
- if (existing) {
72
- existing.enumValues.push(...enumValues)
73
- } else {
74
- childMap.set(refNode.name, {
75
- propertyName: discriminatorPropertyName,
76
- enumValues: [...enumValues],
77
- })
64
+ if (!existing) {
65
+ childMap.set(refNode.name, { propertyName: discriminatorPropertyName, enumValues: [...enumValues] })
66
+ continue
78
67
  }
68
+ existing.enumValues.push(...enumValues)
79
69
  }
80
70
  }
81
71
 
82
- if (childMap.size === 0) return root
72
+ return childMap
73
+ }
83
74
 
84
- return ast.transform(root, {
85
- schema(node, { parent }) {
86
- if (parent?.kind !== 'Input' || !node.name) return
75
+ /**
76
+ * Patches a single top-level `SchemaNode` with its discriminator entry (adds or replaces
77
+ * the discriminant property). Used by the streaming path to apply patches inline per yield
78
+ * without buffering all schemas.
79
+ */
80
+ export function patchDiscriminatorNode(node: ast.SchemaNode, entry: { propertyName: string; enumValues: Array<string | number | boolean> }): ast.SchemaNode {
81
+ const objectNode = ast.narrowSchema(node, 'object')
82
+ if (!objectNode) return node
83
+
84
+ const { propertyName, enumValues } = entry
85
+ const enumSchema = ast.factory.createSchema({ type: 'enum', enumValues })
86
+ const newProp = ast.factory.createProperty({ name: propertyName, required: true, schema: enumSchema })
87
87
 
88
- const entry = childMap.get(node.name)
89
- if (!entry) return
88
+ const existingIdx = objectNode.properties.findIndex((p) => p.name === propertyName)
89
+ const newProperties = existingIdx >= 0 ? objectNode.properties.map((p, i) => (i === existingIdx ? newProp : p)) : [...objectNode.properties, newProp]
90
90
 
91
- const objectNode = ast.narrowSchema(node, 'object')
92
- if (!objectNode) return
91
+ return { ...objectNode, properties: newProperties }
92
+ }
93
93
 
94
- const { propertyName, enumValues } = entry
95
- const enumSchema = ast.createSchema({ type: 'enum', enumValues })
96
- const newProp = ast.createProperty({
94
+ /**
95
+ * Creates a single-property object schema used as a discriminator literal.
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * createDiscriminantNode({ propertyName: 'type', value: 'dog' })
100
+ * // -> { type: 'object', properties: [{ name: 'type', required: true, schema: enum('dog') }] }
101
+ * ```
102
+ */
103
+ export function createDiscriminantNode({ propertyName, value }: { propertyName: string; value: string }): ast.SchemaNode {
104
+ return ast.factory.createSchema({
105
+ type: 'object',
106
+ primitive: 'object',
107
+ properties: [
108
+ ast.factory.createProperty({
97
109
  name: propertyName,
110
+ schema: ast.factory.createSchema({
111
+ type: 'enum',
112
+ primitive: 'string',
113
+ enumValues: [value],
114
+ }),
98
115
  required: true,
99
- schema: enumSchema,
100
- })
101
-
102
- const existingIdx = objectNode.properties.findIndex((p) => p.name === propertyName)
103
- const newProperties = existingIdx >= 0 ? objectNode.properties.map((p, i) => (i === existingIdx ? newProp : p)) : [...objectNode.properties, newProp]
104
-
105
- return { ...objectNode, properties: newProperties }
106
- },
116
+ }),
117
+ ],
107
118
  })
108
119
  }
120
+
121
+ /**
122
+ * Returns the discriminator key whose mapping value matches `ref`, or `null` when there is no match.
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * findDiscriminator({ dog: '#/components/schemas/Dog' }, '#/components/schemas/Dog') // 'dog'
127
+ * ```
128
+ */
129
+ export function findDiscriminator(mapping: Record<string, string> | undefined, ref: string | undefined): string | null {
130
+ if (!mapping || !ref) return null
131
+ return Object.entries(mapping).find(([, value]) => value === ref)?.[0] ?? null
132
+ }
package/src/factory.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  import path from 'node:path'
2
- import { mergeDeep, URLPath } from '@internals/utils'
2
+ import { exists, mergeDeep, Url } from '@internals/utils'
3
+ import { Diagnostics } from '@kubb/core'
3
4
  import type { AdapterSource } from '@kubb/core'
4
- import { bundle, loadConfig } from '@redocly/openapi-core'
5
- import OASNormalize from 'oas-normalize'
6
- import swagger2openapi from 'swagger2openapi'
5
+ import { compileErrors, validate } from '@readme/openapi-parser'
6
+ import { parse } from 'yaml'
7
+ import { bundleDocument } from './bundler.ts'
7
8
  import { MERGE_DEFAULT_TITLE, MERGE_DEFAULT_VERSION, MERGE_OPENAPI_VERSION } from './constants.ts'
8
9
  import { isOpenApiV2Document } from './guards.ts'
9
10
  import type { Document } from './types.ts'
10
11
 
11
12
  export type ParseOptions = {
12
13
  canBundle?: boolean
13
- enablePaths?: boolean
14
14
  }
15
15
 
16
16
  export type ValidateDocumentOptions = {
@@ -20,9 +20,10 @@ export type ValidateDocumentOptions = {
20
20
  /**
21
21
  * Loads and dereferences an OpenAPI document, returning the raw `Document`.
22
22
  *
23
- * Accepts a file path string or an already-parsed document object. File paths are bundled via
24
- * Redocly to resolve external `$ref`s. Swagger 2.0 documents are automatically up-converted
25
- * to OpenAPI 3.0 via `swagger2openapi`.
23
+ * Accepts a file path string or an already-parsed document object. File paths and URLs are
24
+ * bundled via `api-ref-bundler`, hoisting external file schemas into named `components.schemas`
25
+ * entries so generators can emit named types and imports. Swagger 2.0 documents are
26
+ * automatically up-converted to OpenAPI 3.0 via `swagger2openapi`.
26
27
  *
27
28
  * @example
28
29
  * ```ts
@@ -30,28 +31,19 @@ export type ValidateDocumentOptions = {
30
31
  * const document = await parse(rawDocumentObject, { canBundle: false })
31
32
  * ```
32
33
  */
33
- export async function parseDocument(pathOrApi: string | Document, { canBundle = true, enablePaths = true }: ParseOptions = {}): Promise<Document> {
34
+ export async function parseDocument(pathOrApi: string | Document, { canBundle = true }: ParseOptions = {}): Promise<Document> {
34
35
  if (typeof pathOrApi === 'string' && canBundle) {
35
- const config = await loadConfig()
36
- const bundleResults = await bundle({
37
- ref: pathOrApi,
38
- config,
39
- base: pathOrApi,
40
- })
36
+ const bundled = await bundleDocument(pathOrApi)
41
37
 
42
- return parseDocument(bundleResults.bundle.parsed as string, {
43
- canBundle,
44
- enablePaths,
45
- })
38
+ return parseDocument(bundled, { canBundle: false })
46
39
  }
47
40
 
48
- const oasNormalize = new OASNormalize(pathOrApi, {
49
- enablePaths,
50
- colorizeErrors: true,
51
- })
52
- const document = (await oasNormalize.load()) as Document
41
+ // A string here is always inline YAML/JSON content: file paths and URLs are read and parsed by
42
+ // `bundleDocument` first. `yaml.parse` also parses JSON, since JSON is a subset of YAML.
43
+ const document = (typeof pathOrApi === 'string' ? parse(pathOrApi) : pathOrApi) as Document
53
44
 
54
45
  if (isOpenApiV2Document(document)) {
46
+ const { default: swagger2openapi } = await import('swagger2openapi')
55
47
  const { openapi } = await swagger2openapi.convertObj(document, {
56
48
  anchors: true,
57
49
  })
@@ -65,7 +57,7 @@ export async function parseDocument(pathOrApi: string | Document, { canBundle =
65
57
  /**
66
58
  * Deep-merges multiple OpenAPI documents into a single `Document`.
67
59
  *
68
- * Each document is parsed independently then recursively merged via `mergeDeep` from `@internals/utils`.
60
+ * Each document is parsed independently, then deep-merged into one in array order.
69
61
  * Throws when the input array is empty.
70
62
  *
71
63
  * @example
@@ -74,13 +66,16 @@ export async function parseDocument(pathOrApi: string | Document, { canBundle =
74
66
  * ```
75
67
  */
76
68
  export async function mergeDocuments(pathOrApi: Array<string | Document>): Promise<Document> {
77
- const documents: Document[] = []
78
- for (const p of pathOrApi) {
79
- documents.push(await parseDocument(p, { enablePaths: false, canBundle: false }))
80
- }
69
+ const documents = await Promise.all(pathOrApi.map((p) => parseDocument(p, { canBundle: false })))
81
70
 
82
71
  if (documents.length === 0) {
83
- throw new Error('No OAS documents provided for merging.')
72
+ throw new Diagnostics.Error({
73
+ code: Diagnostics.code.inputRequired,
74
+ severity: 'error',
75
+ message: 'No OAS documents were provided for merging.',
76
+ help: 'Pass at least one path or document to `input.path`.',
77
+ location: { kind: 'config' },
78
+ })
84
79
  }
85
80
 
86
81
  const seed: Document = {
@@ -102,9 +97,9 @@ export async function mergeDocuments(pathOrApi: Array<string | Document>): Promi
102
97
  * Creates a `Document` from an `AdapterSource`.
103
98
  *
104
99
  * Handles all three source types:
105
- * - `{ type: 'path' }` resolves and bundles a local file path or remote URL.
106
- * - `{ type: 'paths' }` merges multiple file paths into a single document.
107
- * - `{ type: 'data' }` parses an inline string (YAML/JSON) or raw object.
100
+ * - `{ type: 'path' }` resolves and bundles a local file path or remote URL.
101
+ * - `{ type: 'paths' }` merges multiple file paths into a single document.
102
+ * - `{ type: 'data' }` parses an inline string (YAML/JSON) or raw object.
108
103
  *
109
104
  * @example
110
105
  * ```ts
@@ -112,7 +107,7 @@ export async function mergeDocuments(pathOrApi: Array<string | Document>): Promi
112
107
  * const document = await parseFromConfig({ type: 'data', data: '{"openapi":"3.0.0",...}' })
113
108
  * ```
114
109
  */
115
- export function parseFromConfig(source: AdapterSource): Promise<Document> {
110
+ export async function parseFromConfig(source: AdapterSource): Promise<Document> {
116
111
  if (source.type === 'data') {
117
112
  if (typeof source.data === 'object') {
118
113
  return parseDocument(structuredClone(source.data) as Document)
@@ -126,15 +121,37 @@ export function parseFromConfig(source: AdapterSource): Promise<Document> {
126
121
  }
127
122
 
128
123
  // type === 'path'
129
- if (new URLPath(source.path).isURL) {
124
+ if (Url.canParse(source.path)) {
130
125
  return parseDocument(source.path)
131
126
  }
132
127
 
133
- return parseDocument(path.resolve(path.dirname(source.path), source.path))
128
+ const resolved = path.resolve(path.dirname(source.path), source.path)
129
+ await assertInputExists(resolved)
130
+ return parseDocument(resolved)
134
131
  }
135
132
 
136
133
  /**
137
- * Validates an OpenAPI document using `oas-normalize` with colorized error output.
134
+ * Throws a coded `KUBB_INPUT_NOT_FOUND` diagnostic when a local input path does not exist.
135
+ * URLs are skipped, and a malformed but readable file is left for `parseDocument` to surface
136
+ * its parse error instead.
137
+ */
138
+ export async function assertInputExists(input: string): Promise<void> {
139
+ if (Url.canParse(input)) {
140
+ return
141
+ }
142
+ if (!(await exists(input))) {
143
+ throw new Diagnostics.Error({
144
+ code: Diagnostics.code.inputNotFound,
145
+ severity: 'error',
146
+ message: `Cannot read the file set in \`input.path\` (or via \`kubb generate PATH\`): ${input}`,
147
+ help: 'Check that the path exists and is readable, then set it in `input.path` or pass it as `kubb generate PATH`.',
148
+ location: { kind: 'config' },
149
+ })
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Validates an OpenAPI document using `@readme/openapi-parser` with colorized error output.
138
155
  *
139
156
  * @example
140
157
  * ```ts
@@ -143,23 +160,21 @@ export function parseFromConfig(source: AdapterSource): Promise<Document> {
143
160
  */
144
161
  export async function validateDocument(document: Document, { throwOnError = false }: ValidateDocumentOptions = {}): Promise<void> {
145
162
  try {
146
- const oasNormalize = new OASNormalize(document, {
147
- enablePaths: true,
148
- colorizeErrors: true,
149
- })
150
-
151
- await oasNormalize.validate({
152
- parser: {
153
- validate: {
154
- errors: { colorize: true },
155
- },
163
+ // `validate` dereferences its input in place, so clone to keep the cached document intact.
164
+ const result = await validate(structuredClone(document), {
165
+ validate: {
166
+ errors: { colorize: true },
156
167
  },
157
168
  })
169
+
170
+ if (!result.valid) {
171
+ throw new Error(compileErrors(result))
172
+ }
158
173
  } catch (error) {
159
174
  if (throwOnError) {
160
175
  throw error
161
176
  }
162
177
 
163
- // Validation failures are non-fatal mirror plugin-oas behavior
178
+ // Validation failures are non-fatal, mirror plugin-oas behavior
164
179
  }
165
180
  }
package/src/index.ts CHANGED
@@ -1,6 +1,4 @@
1
1
  export { adapterOas, adapterOasName } from './adapter.ts'
2
- export type { ValidateDocumentOptions } from './factory.ts'
3
- export { mergeDocuments } from './factory.ts'
4
2
  export type {
5
3
  AdapterOas,
6
4
  AdapterOasOptions,
@@ -15,4 +13,3 @@ export type {
15
13
  ResponseObject,
16
14
  SchemaObject,
17
15
  } from './types.ts'
18
- export { HttpMethods } from './types.ts'
package/src/mime.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * MIME type fragments that mark a media type as JSON-like.
3
+ *
4
+ * Matches the substrings used by the `oas` library's `matchesMimeType.json`, so a content type is
5
+ * JSON when it contains any of these. The `+json` entry catches structured-syntax suffixes such as
6
+ * `application/vnd.api+json`.
7
+ */
8
+ const jsonMimeFragments = ['application/json', 'application/x-json', 'text/json', 'text/x-json', '+json'] as const
9
+
10
+ /**
11
+ * Returns `true` when a media type string is JSON-like.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * isJsonMimeType('application/json') // true
16
+ * isJsonMimeType('application/vnd.api+json') // true
17
+ * isJsonMimeType('multipart/form-data') // false
18
+ * ```
19
+ */
20
+ export function isJsonMimeType(mimeType: string): boolean {
21
+ return jsonMimeFragments.some((fragment) => mimeType.includes(fragment))
22
+ }
@@ -0,0 +1,194 @@
1
+ import { SUPPORTED_METHODS } from './constants.ts'
2
+ import { isReference } from './guards.ts'
3
+ import { isJsonMimeType } from './mime.ts'
4
+ import { resolveRef } from './refs.ts'
5
+ import type { Document, MediaTypeObject, OperationObject, PathItemObject, ReferenceObject, RequestBodyObject, ResponseObject } from './types.ts'
6
+
7
+ /**
8
+ * A single OpenAPI operation: its URL path, HTTP method, and the raw operation object.
9
+ *
10
+ * `schema` is a live reference into the document, so any in-place `$ref` resolution the resolvers
11
+ * perform is visible here too.
12
+ */
13
+ export type Operation = {
14
+ path: string
15
+ method: string
16
+ schema: OperationObject
17
+ }
18
+
19
+ /**
20
+ * The document plus the operation being read. Shared by the request/response accessors so they can
21
+ * resolve `$ref`s against the document.
22
+ */
23
+ type OperationContext = {
24
+ document: Document
25
+ operation: Operation
26
+ }
27
+
28
+ /**
29
+ * Slugifies a path for the `operationId` fallback: non-alphanumerics collapse to single dashes,
30
+ * with no leading or trailing dash.
31
+ */
32
+ function slugify(value: string): string {
33
+ return value
34
+ .replace(/[^a-zA-Z0-9]/g, '-')
35
+ .replace(/-{2,}/g, '-')
36
+ .replace(/^-|-$/g, '')
37
+ }
38
+
39
+ /**
40
+ * Returns the operation's `operationId`, falling back to `<method>_<slugified-path>` when absent.
41
+ */
42
+ export function getOperationId({ path, method, schema }: Operation): string {
43
+ const { operationId } = schema
44
+ if (typeof operationId === 'string' && operationId.length > 0) {
45
+ return operationId
46
+ }
47
+ return `${method}_${slugify(path).toLowerCase()}`
48
+ }
49
+
50
+ /**
51
+ * Returns the declared response status codes, skipping `x-` extensions and non-object entries.
52
+ */
53
+ export function getResponseStatusCodes({ schema }: Operation): Array<string> {
54
+ const responses = schema.responses as Record<string, unknown> | undefined
55
+ if (!responses || isReference(responses)) {
56
+ return []
57
+ }
58
+ return Object.keys(responses).filter((key) => !key.startsWith('x-') && !!responses[key] && typeof responses[key] === 'object')
59
+ }
60
+
61
+ /**
62
+ * Returns the response object for a status code, resolving a `$ref` in place. `false` when absent.
63
+ */
64
+ export function getResponseByStatusCode({ document, operation, statusCode }: OperationContext & { statusCode: string | number }): ResponseObject | false {
65
+ const responses = operation.schema.responses as Record<string, ResponseObject | ReferenceObject> | undefined
66
+ if (!responses || isReference(responses)) {
67
+ return false
68
+ }
69
+ const response = responses[statusCode]
70
+ if (!response) {
71
+ return false
72
+ }
73
+ if (isReference(response)) {
74
+ const resolved = resolveRef<ResponseObject>(document, response.$ref)
75
+ responses[statusCode] = resolved as ResponseObject
76
+ if (!resolved || isReference(resolved)) {
77
+ return false
78
+ }
79
+ return resolved
80
+ }
81
+ return response
82
+ }
83
+
84
+ /**
85
+ * Resolves the request body (dereferencing a `$ref` in place) and returns its content map, or
86
+ * `undefined` when the operation has no request body.
87
+ */
88
+ function getRequestBodyContent({ document, operation }: OperationContext): Record<string, MediaTypeObject> | undefined {
89
+ const { schema } = operation
90
+ let requestBody = schema.requestBody as RequestBodyObject | ReferenceObject | undefined
91
+ if (!requestBody) {
92
+ return undefined
93
+ }
94
+ if (isReference(requestBody)) {
95
+ const resolved = resolveRef<RequestBodyObject>(document, requestBody.$ref)
96
+ ;(schema as { requestBody?: unknown }).requestBody = resolved
97
+ if (!resolved || isReference(resolved)) {
98
+ return undefined
99
+ }
100
+ requestBody = resolved
101
+ }
102
+ return requestBody.content
103
+ }
104
+
105
+ /**
106
+ * Returns the request body media type. With `mediaType` set, returns that entry or `false`.
107
+ * Otherwise picks the first JSON-like media type, then the first declared one, as a
108
+ * `[mediaType, object]` tuple.
109
+ */
110
+ export function getRequestContent({
111
+ document,
112
+ operation,
113
+ mediaType,
114
+ }: OperationContext & { mediaType?: string }): MediaTypeObject | false | [string, MediaTypeObject] {
115
+ const content = getRequestBodyContent({ document, operation })
116
+ if (!content) {
117
+ return false
118
+ }
119
+ if (mediaType) {
120
+ return mediaType in content ? content[mediaType]! : false
121
+ }
122
+ const mediaTypes = Object.keys(content)
123
+ const available = mediaTypes.find((mt) => isJsonMimeType(mt)) ?? mediaTypes[0]
124
+ return available ? [available, content[available]!] : false
125
+ }
126
+
127
+ /**
128
+ * Returns the primary request content type. Prefers a JSON-like media type (the last one wins,
129
+ * matching the previous behavior), then the first declared one, defaulting to `'application/json'`.
130
+ */
131
+ export function getRequestContentType({ document, operation }: OperationContext): string {
132
+ const content = getRequestBodyContent({ document, operation })
133
+ const mediaTypes = content ? Object.keys(content) : []
134
+
135
+ let result = mediaTypes[0] ?? 'application/json'
136
+ for (const mt of mediaTypes) {
137
+ if (isJsonMimeType(mt)) {
138
+ result = mt
139
+ }
140
+ }
141
+ return result
142
+ }
143
+
144
+ /**
145
+ * Builds an `Operation` for every supported HTTP method on every path, in document order.
146
+ * `x-` path keys and unresolvable path-item `$ref`s are skipped.
147
+ *
148
+ * @example
149
+ * ```ts
150
+ * for (const operation of getOperations(document)) {
151
+ * parseOperation(options, operation)
152
+ * }
153
+ * ```
154
+ */
155
+ export function getOperations(document: Document): Array<Operation> {
156
+ const operations: Array<Operation> = []
157
+ const paths = document.paths
158
+ if (!paths) {
159
+ return operations
160
+ }
161
+
162
+ for (const path of Object.keys(paths)) {
163
+ if (path.startsWith('x-')) {
164
+ continue
165
+ }
166
+
167
+ let pathItem = paths[path] as PathItemObject | ReferenceObject | undefined
168
+ if (!pathItem) {
169
+ continue
170
+ }
171
+ if (isReference(pathItem)) {
172
+ const resolved = resolveRef<PathItemObject>(document, pathItem.$ref)
173
+ ;(paths as Record<string, unknown>)[path] = resolved
174
+ if (!resolved || isReference(resolved)) {
175
+ continue
176
+ }
177
+ pathItem = resolved
178
+ }
179
+
180
+ const item = pathItem as Record<string, unknown>
181
+ for (const method of Object.keys(item)) {
182
+ if (!SUPPORTED_METHODS.has(method)) {
183
+ continue
184
+ }
185
+ const schema = item[method]
186
+ if (!schema || typeof schema !== 'object') {
187
+ continue
188
+ }
189
+ operations.push({ path, method, schema: schema as OperationObject })
190
+ }
191
+ }
192
+
193
+ return operations
194
+ }