@znight/openapi 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +572 -2
- package/dist/build-BTUC96mY.d.mts +174 -0
- package/dist/build-DXunOzlI.mjs +733 -0
- package/dist/build-DXunOzlI.mjs.map +1 -0
- package/dist/compiler.d.mts +16 -0
- package/dist/compiler.mjs +2639 -0
- package/dist/compiler.mjs.map +1 -0
- package/dist/emit-child.d.mts +1 -0
- package/dist/emit-child.mjs +124 -0
- package/dist/emit-child.mjs.map +1 -0
- package/dist/emit.d.mts +2 -0
- package/dist/emit.mjs +252 -0
- package/dist/emit.mjs.map +1 -0
- package/dist/index-CqQ6IRnN.d.mts +60 -0
- package/dist/index.d.mts +260 -0
- package/dist/index.mjs +777 -0
- package/dist/index.mjs.map +1 -0
- package/dist/openapi-CQk5dHqc.mjs +39 -0
- package/dist/openapi-CQk5dHqc.mjs.map +1 -0
- package/dist/shared-BZxtyaB0.mjs +147 -0
- package/dist/shared-BZxtyaB0.mjs.map +1 -0
- package/dist/testing.d.mts +39 -0
- package/dist/testing.mjs +155 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/types-VzBexMKj.d.mts +213 -0
- package/dist/vite.d.mts +24 -0
- package/dist/vite.mjs +64 -0
- package/dist/vite.mjs.map +1 -0
- package/package.json +115 -4
- package/src/build.ts +898 -0
- package/src/compiler/analyze.ts +81 -0
- package/src/compiler/document.ts +612 -0
- package/src/compiler/generate.ts +79 -0
- package/src/compiler/index.ts +47 -0
- package/src/compiler/model.ts +141 -0
- package/src/compiler/operations.ts +1475 -0
- package/src/compiler/schemas.ts +706 -0
- package/src/compiler/validate.ts +102 -0
- package/src/compiler/values.ts +81 -0
- package/src/converters.ts +137 -0
- package/src/decorators.ts +187 -0
- package/src/emit/child.ts +144 -0
- package/src/emit/index.ts +377 -0
- package/src/emit/protocol.ts +35 -0
- package/src/index.ts +94 -0
- package/src/metadata.ts +112 -0
- package/src/openapi.ts +55 -0
- package/src/plugin.ts +810 -0
- package/src/shared.ts +154 -0
- package/src/testing.ts +224 -0
- package/src/types.ts +240 -0
- package/src/ui.ts +215 -0
- 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
|
+
}
|