@znight/openapi 0.0.0-stage → 0.1.1

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/LICENSE +21 -0
  2. package/README.md +572 -2
  3. package/dist/build-BTUC96mY.d.mts +174 -0
  4. package/dist/build-DXunOzlI.mjs +733 -0
  5. package/dist/build-DXunOzlI.mjs.map +1 -0
  6. package/dist/compiler.d.mts +16 -0
  7. package/dist/compiler.mjs +2639 -0
  8. package/dist/compiler.mjs.map +1 -0
  9. package/dist/emit-child.d.mts +1 -0
  10. package/dist/emit-child.mjs +124 -0
  11. package/dist/emit-child.mjs.map +1 -0
  12. package/dist/emit.d.mts +2 -0
  13. package/dist/emit.mjs +252 -0
  14. package/dist/emit.mjs.map +1 -0
  15. package/dist/index-CqQ6IRnN.d.mts +60 -0
  16. package/dist/index.d.mts +260 -0
  17. package/dist/index.mjs +777 -0
  18. package/dist/index.mjs.map +1 -0
  19. package/dist/openapi-CQk5dHqc.mjs +39 -0
  20. package/dist/openapi-CQk5dHqc.mjs.map +1 -0
  21. package/dist/shared-BZxtyaB0.mjs +147 -0
  22. package/dist/shared-BZxtyaB0.mjs.map +1 -0
  23. package/dist/testing.d.mts +39 -0
  24. package/dist/testing.mjs +155 -0
  25. package/dist/testing.mjs.map +1 -0
  26. package/dist/types-VzBexMKj.d.mts +213 -0
  27. package/dist/vite.d.mts +24 -0
  28. package/dist/vite.mjs +64 -0
  29. package/dist/vite.mjs.map +1 -0
  30. package/package.json +115 -4
  31. package/src/build.ts +898 -0
  32. package/src/compiler/analyze.ts +81 -0
  33. package/src/compiler/document.ts +612 -0
  34. package/src/compiler/generate.ts +79 -0
  35. package/src/compiler/index.ts +47 -0
  36. package/src/compiler/model.ts +141 -0
  37. package/src/compiler/operations.ts +1475 -0
  38. package/src/compiler/schemas.ts +706 -0
  39. package/src/compiler/validate.ts +102 -0
  40. package/src/compiler/values.ts +81 -0
  41. package/src/converters.ts +137 -0
  42. package/src/decorators.ts +187 -0
  43. package/src/emit/child.ts +144 -0
  44. package/src/emit/index.ts +377 -0
  45. package/src/emit/protocol.ts +35 -0
  46. package/src/index.ts +94 -0
  47. package/src/metadata.ts +112 -0
  48. package/src/openapi.ts +55 -0
  49. package/src/plugin.ts +810 -0
  50. package/src/shared.ts +154 -0
  51. package/src/testing.ts +224 -0
  52. package/src/types.ts +240 -0
  53. package/src/ui.ts +215 -0
  54. package/src/vite.ts +126 -0
package/src/shared.ts ADDED
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Import-free helpers shared by the compiler extension and the runtime (the compiler entry may
3
+ * only reach modules of the package that import nothing at runtime).
4
+ */
5
+
6
+ /** Grammar of operation ids. */
7
+ export const OPERATION_ID: RegExp = /^[A-Za-z_][\w.-]*$/
8
+
9
+ /** Grammar of HTTP header names (RFC 9110 token). */
10
+ export const HEADER_NAME: RegExp = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/
11
+
12
+ /** A media type or a range (`application/json`, `image/*`, `*\/*`), parameters allowed. */
13
+ export const MEDIA_TYPE: RegExp =
14
+ /^(?:\*\/\*|[\w!#$&^.+-]+\/(?:\*|[\w!#$&^.+-]+))(?:\s*;\s*[\w!#$&^.+-]+=(?:[\w!#$&^.+-]+|"[^"]*"))*$/
15
+
16
+ /** OpenAPI methods, in document order. */
17
+ export const METHOD_ORDER: readonly string[] = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch']
18
+
19
+ /** Reserved names of the standard error components. */
20
+ export const ERROR_COMPONENTS: { readonly body: 'HttpErrorBody'; readonly validation: 'ValidationErrorBody' } =
21
+ Object.freeze({ body: 'HttpErrorBody', validation: 'ValidationErrorBody' })
22
+
23
+ /** Standard responses referenced by operations (`#/components/responses/<name>`). */
24
+ export const ERROR_RESPONSES: {
25
+ readonly badRequest: 'BadRequest'
26
+ readonly payloadTooLarge: 'PayloadTooLarge'
27
+ readonly unsupportedMediaType: 'UnsupportedMediaType'
28
+ } = Object.freeze({
29
+ badRequest: 'BadRequest',
30
+ payloadTooLarge: 'PayloadTooLarge',
31
+ unsupportedMediaType: 'UnsupportedMediaType',
32
+ })
33
+
34
+ /** The OpenAPI version of the documents. */
35
+ export const OPENAPI_VERSION = '3.1.1'
36
+
37
+ /** A component name: `[A-Za-z0-9._-]`, other characters replaced with `_`. */
38
+ export function componentName(name: string): string {
39
+ const cleaned = name.replace(/[^A-Za-z0-9._-]/g, '_')
40
+ return cleaned === '' ? '_' : cleaned
41
+ }
42
+
43
+ /** `#/components/schemas/<name>`. */
44
+ export function schemaRef(name: string): string {
45
+ return `#/components/schemas/${name}`
46
+ }
47
+
48
+ /** Code-point comparison of strings (deterministic across runtimes and locales). */
49
+ export function compareStrings(a: string, b: string): number {
50
+ if (a === b) return 0
51
+ return a < b ? -1 : 1
52
+ }
53
+
54
+ /** Deep equality of JSON-like values (key order ignored). */
55
+ export function jsonEquals(a: unknown, b: unknown): boolean {
56
+ if (a === b) return true
57
+ if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false
58
+ if (Array.isArray(a) !== Array.isArray(b)) return false
59
+ if (Array.isArray(a) && Array.isArray(b)) {
60
+ return a.length === b.length && a.every((item, index) => jsonEquals(item, b[index]))
61
+ }
62
+ const keysA = Object.keys(a).filter(key => (a as Record<string, unknown>)[key] !== undefined)
63
+ const keysB = Object.keys(b).filter(key => (b as Record<string, unknown>)[key] !== undefined)
64
+ if (keysA.length !== keysB.length) return false
65
+ return keysA.every(key => jsonEquals((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]))
66
+ }
67
+
68
+ /** JSON text with sorted object keys (fingerprints). */
69
+ export function canonicalJson(value: unknown): string {
70
+ return JSON.stringify(sortKeys(value))
71
+ }
72
+
73
+ function sortKeys(value: unknown): unknown {
74
+ if (Array.isArray(value)) return value.map(sortKeys)
75
+ if (typeof value !== 'object' || value === null) return value
76
+ const sorted: Record<string, unknown> = {}
77
+ for (const key of Object.keys(value).sort(compareStrings)) {
78
+ const item = (value as Record<string, unknown>)[key]
79
+ if (item !== undefined) sorted[key] = sortKeys(item)
80
+ }
81
+ return sorted
82
+ }
83
+
84
+ /** Reason phrases of the statuses documented by default (`description` of responses). */
85
+ const REASONS: Readonly<Record<number, string>> = {
86
+ 100: 'Continue',
87
+ 101: 'Switching Protocols',
88
+ 200: 'OK',
89
+ 201: 'Created',
90
+ 202: 'Accepted',
91
+ 203: 'Non-Authoritative Information',
92
+ 204: 'No Content',
93
+ 205: 'Reset Content',
94
+ 206: 'Partial Content',
95
+ 207: 'Multi-Status',
96
+ 300: 'Multiple Choices',
97
+ 301: 'Moved Permanently',
98
+ 302: 'Found',
99
+ 303: 'See Other',
100
+ 304: 'Not Modified',
101
+ 307: 'Temporary Redirect',
102
+ 308: 'Permanent Redirect',
103
+ 400: 'Bad Request',
104
+ 401: 'Unauthorized',
105
+ 402: 'Payment Required',
106
+ 403: 'Forbidden',
107
+ 404: 'Not Found',
108
+ 405: 'Method Not Allowed',
109
+ 406: 'Not Acceptable',
110
+ 408: 'Request Timeout',
111
+ 409: 'Conflict',
112
+ 410: 'Gone',
113
+ 411: 'Length Required',
114
+ 412: 'Precondition Failed',
115
+ 413: 'Content Too Large',
116
+ 414: 'URI Too Long',
117
+ 415: 'Unsupported Media Type',
118
+ 416: 'Range Not Satisfiable',
119
+ 417: 'Expectation Failed',
120
+ 418: "I'm a teapot",
121
+ 421: 'Misdirected Request',
122
+ 422: 'Unprocessable Content',
123
+ 423: 'Locked',
124
+ 424: 'Failed Dependency',
125
+ 425: 'Too Early',
126
+ 426: 'Upgrade Required',
127
+ 428: 'Precondition Required',
128
+ 429: 'Too Many Requests',
129
+ 431: 'Request Header Fields Too Large',
130
+ 451: 'Unavailable For Legal Reasons',
131
+ 500: 'Internal Server Error',
132
+ 501: 'Not Implemented',
133
+ 502: 'Bad Gateway',
134
+ 503: 'Service Unavailable',
135
+ 504: 'Gateway Timeout',
136
+ 505: 'HTTP Version Not Supported',
137
+ }
138
+
139
+ /** Description of a response without one: its reason phrase (`'2XX'` → `Success`). */
140
+ export function reasonPhrase(status: string): string {
141
+ if (status === 'default') return 'Unexpected response'
142
+ const range = /^([1-5])XX$/.exec(status)
143
+ if (range) {
144
+ return { '1': 'Informational', '2': 'Success', '3': 'Redirection', '4': 'Client error', '5': 'Server error' }[
145
+ range[1] as '1' | '2' | '3' | '4' | '5'
146
+ ]
147
+ }
148
+ return REASONS[Number(status)] ?? `Status ${status}`
149
+ }
150
+
151
+ /** `200`-`599` codes, `1XX`-`5XX` ranges, or `default`. */
152
+ export function isResponseStatus(status: string): boolean {
153
+ return status === 'default' || /^[1-5]XX$/.test(status) || (/^[1-5]\d\d$/.test(status) && Number(status) >= 100)
154
+ }
package/src/testing.ts ADDED
@@ -0,0 +1,224 @@
1
+ /**
2
+ * `@znight/openapi/testing`: checks that a response matches its documentation (status, media type,
3
+ * documented headers, JSON body against the resolved schema). JSON Schema 2020-12 validation uses
4
+ * the optional peer `@cfworker/json-schema` (no code generation: Bun, Node.js and edge runtimes).
5
+ */
6
+ import type { JsonSchema, OpenApiObject, ReferenceObject, ResponseObject } from './types.ts'
7
+
8
+ import { createRequire } from 'node:module'
9
+
10
+ /** A problem found by {@link validateResponse}. */
11
+ export interface ResponseIssue {
12
+ /** `status`, `headers.location`, `body.items[0].id`... */
13
+ readonly path: string
14
+ readonly message: string
15
+ /** `operation`, `status`, `media-type`, `header`, `json`, or the failing JSON Schema keyword (`type`, `required`...). */
16
+ readonly keyword: string
17
+ }
18
+
19
+ /** A response: an `InjectResponse` of `@znight/http` / `@znight/testing`, or a plain object. */
20
+ export interface ResponseLike {
21
+ readonly status: number
22
+ readonly headers: Readonly<Record<string, string | readonly string[] | undefined>>
23
+ readonly body: string
24
+ }
25
+
26
+ /** What to check. */
27
+ export interface ValidateResponseInput {
28
+ /** HTTP method (`'GET'`). */
29
+ readonly method: string
30
+ /** Path template of the document (`/books/{id}`). */
31
+ readonly path: string
32
+ readonly response: ResponseLike
33
+ }
34
+
35
+ interface CfValidator {
36
+ validate(instance: unknown): {
37
+ valid: boolean
38
+ errors: { keyword: string; instanceLocation: string; error: string }[]
39
+ }
40
+ }
41
+ type CfValidatorClass = new (schema: object, draft?: string, shortCircuit?: boolean) => CfValidator
42
+
43
+ let validatorClass: CfValidatorClass | undefined
44
+
45
+ function loadValidator(): CfValidatorClass {
46
+ if (validatorClass !== undefined) return validatorClass
47
+ try {
48
+ const module = createRequire(import.meta.url)('@cfworker/json-schema') as { Validator?: CfValidatorClass }
49
+ if (typeof module.Validator !== 'function') throw new Error('no Validator export')
50
+ validatorClass = module.Validator
51
+ return validatorClass
52
+ } catch (error) {
53
+ throw new Error(
54
+ 'validateResponse() needs the optional peer dependency @cfworker/json-schema (npm install -D @cfworker/json-schema)',
55
+ {
56
+ cause: error,
57
+ },
58
+ )
59
+ }
60
+ }
61
+
62
+ const COMPOSITE = new Set([
63
+ 'properties',
64
+ 'items',
65
+ 'prefixItems',
66
+ '$ref',
67
+ 'allOf',
68
+ 'patternProperties',
69
+ 'dependentSchemas',
70
+ '$dynamicRef',
71
+ ])
72
+
73
+ /**
74
+ * Checks `response` against the documentation of `method path` in `document`. Returns `[]` when it
75
+ * matches.
76
+ *
77
+ * ```ts
78
+ * const response = await app.inject({ method: 'GET', url: '/books/1' })
79
+ * expect(validateResponse(document, { method: 'GET', path: '/books/{id}', response })).toEqual([])
80
+ * ```
81
+ *
82
+ * The status is looked up as an exact code, then its range (`4XX`), then `default`; the media type
83
+ * against the documented ones (ranges such as `image/*` included); documented headers marked
84
+ * `required` must be present; a JSON body is validated against its schema (`$ref`s to the
85
+ * components followed).
86
+ */
87
+ export function validateResponse(document: OpenApiObject, input: ValidateResponseInput): ResponseIssue[] {
88
+ const issues: ResponseIssue[] = []
89
+ const method = input.method.toLowerCase() as 'get'
90
+ const operation = document.paths?.[input.path]?.[method]
91
+ if (operation === undefined) {
92
+ return [
93
+ {
94
+ path: 'operation',
95
+ keyword: 'operation',
96
+ message: `${input.method.toUpperCase()} ${input.path} is not documented`,
97
+ },
98
+ ]
99
+ }
100
+ const status = String(input.response.status)
101
+ const responses = operation.responses ?? {}
102
+ const entry = responses[status] ?? responses[`${status.charAt(0)}XX`] ?? responses['default']
103
+ if (entry === undefined) {
104
+ return [
105
+ {
106
+ path: 'status',
107
+ keyword: 'status',
108
+ message: `The status ${status} is not documented (documented: ${Object.keys(responses).join(', ') || 'none'})`,
109
+ },
110
+ ]
111
+ }
112
+ const documented = resolveResponse(document, entry)
113
+ if (documented === undefined) {
114
+ return [
115
+ {
116
+ path: 'status',
117
+ keyword: '$ref',
118
+ message: `Cannot resolve the response ${JSON.stringify((entry as ReferenceObject).$ref)}`,
119
+ },
120
+ ]
121
+ }
122
+ // headers
123
+ const headers = lowerCase(input.response.headers)
124
+ for (const [name, header] of Object.entries(documented.headers ?? {})) {
125
+ if ((header as { required?: boolean }).required === true && headers[name.toLowerCase()] === undefined) {
126
+ issues.push({
127
+ path: `headers.${name.toLowerCase()}`,
128
+ keyword: 'header',
129
+ message: `The documented header "${name}" is missing`,
130
+ })
131
+ }
132
+ }
133
+ // media type
134
+ const content = documented.content
135
+ const body = input.response.body
136
+ const contentType = headers['content-type']
137
+ const mediaType = (Array.isArray(contentType) ? contentType[0] : contentType)?.split(';')[0]?.trim().toLowerCase()
138
+ if (content === undefined || Object.keys(content).length === 0) {
139
+ if (body !== '')
140
+ issues.push({
141
+ path: 'body',
142
+ keyword: 'content',
143
+ message: `The ${status} response is documented without body, but it has one`,
144
+ })
145
+ return issues
146
+ }
147
+ if (mediaType === undefined) {
148
+ if (body !== '')
149
+ issues.push({
150
+ path: 'headers.content-type',
151
+ keyword: 'media-type',
152
+ message: 'The response has a body but no content-type',
153
+ })
154
+ return issues
155
+ }
156
+ const key = Object.keys(content).find(candidate => mediaMatches(candidate, mediaType))
157
+ if (key === undefined) {
158
+ issues.push({
159
+ path: 'headers.content-type',
160
+ keyword: 'media-type',
161
+ message: `The media type ${mediaType} is not documented (documented: ${Object.keys(content).join(', ')})`,
162
+ })
163
+ return issues
164
+ }
165
+ const schema = content[key]?.schema
166
+ if (schema === undefined || !isJsonMediaType(mediaType)) return issues
167
+ let value: unknown
168
+ try {
169
+ value = JSON.parse(body)
170
+ } catch {
171
+ issues.push({ path: 'body', keyword: 'json', message: 'The body is not valid JSON' })
172
+ return issues
173
+ }
174
+ const Validator = loadValidator()
175
+ // the validator annotates the schemas it reads: work on a copy of the (frozen) document parts
176
+ const root = structuredClone({ ...(schema as JsonSchema), components: document.components ?? {} })
177
+ const result = new Validator(root, '2020-12', false).validate(value)
178
+ if (result.valid) return issues
179
+ const leaves = result.errors.filter(error => !COMPOSITE.has(error.keyword))
180
+ for (const error of leaves.length > 0 ? leaves : result.errors) {
181
+ issues.push({ path: readablePath(error.instanceLocation), keyword: error.keyword, message: error.error })
182
+ }
183
+ return issues
184
+ }
185
+
186
+ function resolveResponse(document: OpenApiObject, entry: ResponseObject | ReferenceObject): ResponseObject | undefined {
187
+ let current: ResponseObject | ReferenceObject | undefined = entry
188
+ for (let depth = 0; depth < 8 && current !== undefined && '$ref' in current; depth++) {
189
+ const name: string = current.$ref.replace(/^#\/components\/responses\//, '')
190
+ current = document.components?.responses?.[name]
191
+ }
192
+ return current === undefined || '$ref' in current ? undefined : current
193
+ }
194
+
195
+ function lowerCase(headers: ResponseLike['headers']): Record<string, string | readonly string[] | undefined> {
196
+ const result: Record<string, string | readonly string[] | undefined> = {}
197
+ for (const [name, value] of Object.entries(headers)) result[name.toLowerCase()] = value
198
+ return result
199
+ }
200
+
201
+ function mediaMatches(documented: string, actual: string): boolean {
202
+ const pattern = documented.split(';')[0]?.trim().toLowerCase() ?? ''
203
+ if (pattern === '*/*' || pattern === actual) return true
204
+ return pattern.endsWith('/*') && actual.startsWith(pattern.slice(0, -1))
205
+ }
206
+
207
+ function isJsonMediaType(mediaType: string): boolean {
208
+ return mediaType === 'application/json' || mediaType.endsWith('+json')
209
+ }
210
+
211
+ /** `#/items/0/id` → `body.items[0].id`. */
212
+ function readablePath(pointer: string): string {
213
+ const segments = pointer
214
+ .replace(/^#\/?/, '')
215
+ .split('/')
216
+ .filter(segment => segment !== '')
217
+ .map(segment => segment.replaceAll('~1', '/').replaceAll('~0', '~'))
218
+ let path = 'body'
219
+ for (const segment of segments)
220
+ if (/^\d+$/.test(segment)) path += `[${segment}]`
221
+ else if (/^[A-Za-z_$][\w$]*$/.test(segment)) path += `.${segment}`
222
+ else path += `[${JSON.stringify(segment)}]`
223
+ return path
224
+ }
package/src/types.ts ADDED
@@ -0,0 +1,240 @@
1
+ /**
2
+ * OpenAPI 3.1 types (our own, no `openapi-types` dependency). They describe the documents produced
3
+ * by `buildOpenApiDocument`: every object is read-only, optional fields are omitted when absent and
4
+ * `x-*` specification extensions are allowed where OpenAPI allows them.
5
+ *
6
+ * Schemas are JSON Schema 2020-12 ({@link JsonSchema}), the dialect of OpenAPI 3.1.
7
+ */
8
+
9
+ /** A JSON value. */
10
+ export type JsonValue = string | number | boolean | null | readonly JsonValue[] | { readonly [key: string]: JsonValue }
11
+
12
+ /** A JSON Schema 2020-12 object (keywords are not modelled one by one). */
13
+ export interface JsonSchema {
14
+ readonly [keyword: string]: JsonValue | undefined
15
+ }
16
+
17
+ /** `x-*` fields. */
18
+ export interface SpecificationExtensions {
19
+ readonly [extension: `x-${string}`]: JsonValue | undefined
20
+ }
21
+
22
+ /** `{ $ref }` to a component (`#/components/schemas/Book`). */
23
+ export interface ReferenceObject {
24
+ readonly $ref: string
25
+ readonly summary?: string
26
+ readonly description?: string
27
+ }
28
+
29
+ export interface ContactObject extends SpecificationExtensions {
30
+ readonly name?: string
31
+ readonly url?: string
32
+ readonly email?: string
33
+ }
34
+
35
+ export interface LicenseObject extends SpecificationExtensions {
36
+ readonly name: string
37
+ /** SPDX expression (exclusive with `url`). */
38
+ readonly identifier?: string
39
+ readonly url?: string
40
+ }
41
+
42
+ export interface InfoObject extends SpecificationExtensions {
43
+ readonly title: string
44
+ readonly version: string
45
+ readonly summary?: string
46
+ readonly description?: string
47
+ readonly termsOfService?: string
48
+ readonly contact?: ContactObject
49
+ readonly license?: LicenseObject
50
+ }
51
+
52
+ export interface ServerVariableObject extends SpecificationExtensions {
53
+ readonly enum?: readonly string[]
54
+ readonly default: string
55
+ readonly description?: string
56
+ }
57
+
58
+ export interface ServerObject extends SpecificationExtensions {
59
+ readonly url: string
60
+ readonly description?: string
61
+ readonly variables?: Readonly<Record<string, ServerVariableObject>>
62
+ }
63
+
64
+ export interface ExternalDocumentationObject extends SpecificationExtensions {
65
+ readonly url: string
66
+ readonly description?: string
67
+ }
68
+
69
+ export interface TagObject extends SpecificationExtensions {
70
+ readonly name: string
71
+ readonly description?: string
72
+ readonly externalDocs?: ExternalDocumentationObject
73
+ }
74
+
75
+ /** Parameter locations. */
76
+ export type ParameterLocation = 'path' | 'query' | 'header' | 'cookie'
77
+
78
+ export interface ExampleObject extends SpecificationExtensions {
79
+ readonly summary?: string
80
+ readonly description?: string
81
+ readonly value?: JsonValue
82
+ readonly externalValue?: string
83
+ }
84
+
85
+ export interface ParameterObjectOf<S> extends SpecificationExtensions {
86
+ readonly name: string
87
+ readonly in: ParameterLocation
88
+ readonly description?: string
89
+ readonly required?: boolean
90
+ readonly deprecated?: boolean
91
+ readonly allowEmptyValue?: boolean
92
+ readonly style?: string
93
+ readonly explode?: boolean
94
+ readonly schema?: S
95
+ readonly example?: JsonValue
96
+ readonly examples?: Readonly<Record<string, ExampleObject | ReferenceObject>>
97
+ }
98
+
99
+ export interface HeaderObjectOf<S> extends SpecificationExtensions {
100
+ readonly description?: string
101
+ readonly required?: boolean
102
+ readonly deprecated?: boolean
103
+ readonly schema?: S
104
+ readonly example?: JsonValue
105
+ }
106
+
107
+ export interface EncodingObjectOf<S> extends SpecificationExtensions {
108
+ readonly contentType?: string
109
+ readonly headers?: Readonly<Record<string, HeaderObjectOf<S> | ReferenceObject>>
110
+ readonly style?: string
111
+ readonly explode?: boolean
112
+ }
113
+
114
+ export interface MediaTypeObjectOf<S> extends SpecificationExtensions {
115
+ readonly schema?: S
116
+ readonly example?: JsonValue
117
+ readonly examples?: Readonly<Record<string, ExampleObject | ReferenceObject>>
118
+ readonly encoding?: Readonly<Record<string, EncodingObjectOf<S>>>
119
+ }
120
+
121
+ export interface RequestBodyObjectOf<S> extends SpecificationExtensions {
122
+ readonly description?: string
123
+ readonly content: Readonly<Record<string, MediaTypeObjectOf<S>>>
124
+ readonly required?: boolean
125
+ }
126
+
127
+ export interface ResponseObjectOf<S> extends SpecificationExtensions {
128
+ readonly description: string
129
+ readonly headers?: Readonly<Record<string, HeaderObjectOf<S> | ReferenceObject>>
130
+ readonly content?: Readonly<Record<string, MediaTypeObjectOf<S>>>
131
+ }
132
+
133
+ /** `{ schemeName: scopes }`: every scheme of one requirement applies (AND). */
134
+ export interface SecurityRequirementObject {
135
+ readonly [scheme: string]: readonly string[]
136
+ }
137
+
138
+ /** An OpenAPI Operation Object whose schemas are `S` and whose parameters may also be `P`. */
139
+ export interface OperationObjectOf<S, P = never> extends SpecificationExtensions {
140
+ readonly tags?: readonly string[]
141
+ readonly summary?: string
142
+ readonly description?: string
143
+ readonly externalDocs?: ExternalDocumentationObject
144
+ readonly operationId?: string
145
+ readonly parameters?: readonly (ParameterObjectOf<S> | ReferenceObject | P)[]
146
+ readonly requestBody?: RequestBodyObjectOf<S> | ReferenceObject
147
+ readonly responses?: Readonly<Record<string, ResponseObjectOf<S> | ReferenceObject>>
148
+ readonly deprecated?: boolean
149
+ readonly security?: readonly SecurityRequirementObject[]
150
+ readonly servers?: readonly ServerObject[]
151
+ }
152
+
153
+ export type ParameterObject = ParameterObjectOf<JsonSchema>
154
+ export type HeaderObject = HeaderObjectOf<JsonSchema>
155
+ export type MediaTypeObject = MediaTypeObjectOf<JsonSchema>
156
+ export type RequestBodyObject = RequestBodyObjectOf<JsonSchema>
157
+ export type ResponseObject = ResponseObjectOf<JsonSchema>
158
+ export type OperationObject = OperationObjectOf<JsonSchema>
159
+
160
+ /** HTTP methods of a path item, in OpenAPI order. */
161
+ export type OpenApiMethod = 'get' | 'put' | 'post' | 'delete' | 'options' | 'head' | 'patch'
162
+
163
+ export interface PathItemObject extends SpecificationExtensions {
164
+ readonly summary?: string
165
+ readonly description?: string
166
+ readonly get?: OperationObject
167
+ readonly put?: OperationObject
168
+ readonly post?: OperationObject
169
+ readonly delete?: OperationObject
170
+ readonly options?: OperationObject
171
+ readonly head?: OperationObject
172
+ readonly patch?: OperationObject
173
+ readonly trace?: OperationObject
174
+ readonly servers?: readonly ServerObject[]
175
+ readonly parameters?: readonly (ParameterObject | ReferenceObject)[]
176
+ }
177
+
178
+ export interface OAuthFlowObject extends SpecificationExtensions {
179
+ readonly authorizationUrl?: string
180
+ readonly tokenUrl?: string
181
+ readonly refreshUrl?: string
182
+ readonly scopes: Readonly<Record<string, string>>
183
+ }
184
+
185
+ export interface OAuthFlowsObject extends SpecificationExtensions {
186
+ readonly implicit?: OAuthFlowObject
187
+ readonly password?: OAuthFlowObject
188
+ readonly clientCredentials?: OAuthFlowObject
189
+ readonly authorizationCode?: OAuthFlowObject
190
+ }
191
+
192
+ /** An OpenAPI Security Scheme Object. */
193
+ export type SecuritySchemeObject =
194
+ | ({
195
+ readonly type: 'apiKey'
196
+ readonly name: string
197
+ readonly in: 'query' | 'header' | 'cookie'
198
+ readonly description?: string
199
+ } & SpecificationExtensions)
200
+ | ({
201
+ readonly type: 'http'
202
+ /** `bearer`, `basic`... (RFC 7235 scheme name). */
203
+ readonly scheme: string
204
+ readonly bearerFormat?: string
205
+ readonly description?: string
206
+ } & SpecificationExtensions)
207
+ | ({
208
+ readonly type: 'oauth2'
209
+ readonly flows: OAuthFlowsObject
210
+ readonly description?: string
211
+ } & SpecificationExtensions)
212
+ | ({
213
+ readonly type: 'openIdConnect'
214
+ readonly openIdConnectUrl: string
215
+ readonly description?: string
216
+ } & SpecificationExtensions)
217
+ | ({ readonly type: 'mutualTLS'; readonly description?: string } & SpecificationExtensions)
218
+
219
+ export interface ComponentsObject extends SpecificationExtensions {
220
+ readonly schemas?: Readonly<Record<string, JsonSchema>>
221
+ readonly responses?: Readonly<Record<string, ResponseObject | ReferenceObject>>
222
+ readonly parameters?: Readonly<Record<string, ParameterObject | ReferenceObject>>
223
+ readonly examples?: Readonly<Record<string, ExampleObject | ReferenceObject>>
224
+ readonly requestBodies?: Readonly<Record<string, RequestBodyObject | ReferenceObject>>
225
+ readonly headers?: Readonly<Record<string, HeaderObject | ReferenceObject>>
226
+ readonly securitySchemes?: Readonly<Record<string, SecuritySchemeObject>>
227
+ }
228
+
229
+ /** An OpenAPI 3.1 document. */
230
+ export interface OpenApiObject extends SpecificationExtensions {
231
+ readonly openapi: string
232
+ readonly info: InfoObject
233
+ readonly jsonSchemaDialect?: string
234
+ readonly servers?: readonly ServerObject[]
235
+ readonly paths?: Readonly<Record<string, PathItemObject>>
236
+ readonly components?: ComponentsObject
237
+ readonly security?: readonly SecurityRequirementObject[]
238
+ readonly tags?: readonly TagObject[]
239
+ readonly externalDocs?: ExternalDocumentationObject
240
+ }