@mpen/routekit 0.1.0 → 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.
- package/dist/bin.d.mts +4 -0
- package/dist/client/react.d.mts +178 -0
- package/dist/client/react.mjs +142 -0
- package/dist/client.d.mts +433 -0
- package/dist/client.mjs +264 -0
- package/dist/content-BuDOmhH_.mjs +102 -0
- package/dist/core-CzUCxvGk.d.mts +140 -0
- package/dist/core-DbmQauwS.mjs +81 -0
- package/dist/handlers.d.mts +72 -0
- package/dist/handlers.mjs +153 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +1152 -0
- package/dist/middleware.d.mts +388 -0
- package/dist/middleware.mjs +1222 -0
- package/dist/request-Dn0zc-xm.mjs +1025 -0
- package/dist/response/content.d.mts +79 -0
- package/dist/response/content.mjs +2 -0
- package/dist/response/json-rpc.d.mts +1 -0
- package/dist/response/json-rpc.mjs +1 -0
- package/dist/response/problem/valibot.d.mts +230 -0
- package/dist/response/problem/valibot.mjs +258 -0
- package/dist/response/problem.d.mts +415 -0
- package/dist/response/problem.mjs +183 -0
- package/dist/response/status.d.mts +45 -0
- package/dist/response/status.mjs +2 -0
- package/dist/responses-B379Ep9Y.d.mts +296 -0
- package/dist/responses-BpVrgeYi.mjs +101 -0
- package/dist/router-Cwb7ak0J.d.mts +1819 -0
- package/dist/routes.d.mts +282 -0
- package/dist/routes.mjs +311 -0
- package/dist/status-C-8mw-FB.mjs +59 -0
- package/dist/valibot-D7liFYyB.d.mts +290 -0
- package/dist/valibot-Du97X-TS.mjs +326 -0
- package/package.json +8 -2
- package/src/bin/gen-api-client.test.ts +0 -70
- package/src/bin/gen-api-client.ts +0 -986
- package/src/client/headers.ts +0 -31
- package/src/client/index.ts +0 -8
- package/src/client/promise.ts +0 -11
- package/src/client/react/index.test.tsx +0 -266
- package/src/client/react/index.ts +0 -431
- package/src/client/responses.test.ts +0 -151
- package/src/client/responses.ts +0 -278
- package/src/client/transport.ts +0 -74
- package/src/client/transports/body-codec.ts +0 -61
- package/src/client/transports/fetch.ts +0 -113
- package/src/client/tsconfig.json +0 -9
- package/src/client/types.ts +0 -15
- package/src/client/url.ts +0 -31
- package/src/index.ts +0 -63
- package/src/router/fetch-types.ts +0 -13
- package/src/router/handlers/index.ts +0 -2
- package/src/router/handlers/openapi/index.ts +0 -2
- package/src/router/handlers/openapi/openapi.ts +0 -293
- package/src/router/integration/zod-openapi.test.ts +0 -74
- package/src/router/lib/charset.test.ts +0 -22
- package/src/router/lib/charset.ts +0 -133
- package/src/router/lib/collections.ts +0 -3
- package/src/router/lib/format.test.ts +0 -67
- package/src/router/lib/format.ts +0 -35
- package/src/router/lib/host.ts +0 -4
- package/src/router/lib/json-schema.ts +0 -6
- package/src/router/lib/media-type.test.ts +0 -122
- package/src/router/lib/media-type.ts +0 -289
- package/src/router/lib/pathname.test.ts +0 -18
- package/src/router/lib/pathname.ts +0 -19
- package/src/router/lib/route-names.ts +0 -70
- package/src/router/lib/route-normalize.test.ts +0 -36
- package/src/router/lib/route-normalize.ts +0 -67
- package/src/router/lib/schema-merge.ts +0 -56
- package/src/router/middleware/accept-ctx.test.ts +0 -33
- package/src/router/middleware/accept-ctx.ts +0 -12
- package/src/router/middleware/body-limit.test.ts +0 -112
- package/src/router/middleware/body-limit.ts +0 -121
- package/src/router/middleware/content-type-context.ts +0 -0
- package/src/router/middleware/cors.test.ts +0 -269
- package/src/router/middleware/cors.ts +0 -490
- package/src/router/middleware/csrf.test.ts +0 -106
- package/src/router/middleware/csrf.ts +0 -192
- package/src/router/middleware/define.ts +0 -249
- package/src/router/middleware/index.ts +0 -34
- package/src/router/middleware/jsxhtml-response.ts +0 -0
- package/src/router/middleware/oas-swagger.ts +0 -0
- package/src/router/middleware/rate-limit.test.ts +0 -886
- package/src/router/middleware/rate-limit.ts +0 -920
- package/src/router/middleware/request-id-ctx.test.ts +0 -183
- package/src/router/middleware/request-id-ctx.ts +0 -135
- package/src/router/middleware/request-logger-format.test.ts +0 -16
- package/src/router/middleware/request-logger-format.ts +0 -269
- package/src/router/middleware/request-logger.test.ts +0 -267
- package/src/router/middleware/request-logger.ts +0 -131
- package/src/router/middleware/start-time-ctx.ts +0 -5
- package/src/router/request.ts +0 -611
- package/src/router/response/core.ts +0 -181
- package/src/router/response/directives.ts +0 -233
- package/src/router/response/formats/content/bodyless.ts +0 -54
- package/src/router/response/formats/content/content.ts +0 -79
- package/src/router/response/formats/content/index.ts +0 -2
- package/src/router/response/formats/json-rpc/index.ts +0 -2
- package/src/router/response/formats/problem/badRequest.ts +0 -90
- package/src/router/response/formats/problem/conflict.ts +0 -90
- package/src/router/response/formats/problem/created.ts +0 -40
- package/src/router/response/formats/problem/index.ts +0 -27
- package/src/router/response/formats/problem/notFound.ts +0 -90
- package/src/router/response/formats/problem/permissionDenied.ts +0 -90
- package/src/router/response/formats/problem/problem.test.ts +0 -888
- package/src/router/response/formats/problem/rateLimited.ts +0 -90
- package/src/router/response/formats/problem/responses.ts +0 -219
- package/src/router/response/formats/problem/root-errors.ts +0 -48
- package/src/router/response/formats/problem/sessionExpired.ts +0 -90
- package/src/router/response/formats/problem/types.ts +0 -170
- package/src/router/response/formats/problem/unauthenticated.ts +0 -90
- package/src/router/response/formats/problem/valibot.ts +0 -410
- package/src/router/response/formats/status/index.ts +0 -1
- package/src/router/response/formats/status/responses.ts +0 -59
- package/src/router/response/formats/status/status.test.ts +0 -21
- package/src/router/response/framers.ts +0 -85
- package/src/router/response/index.ts +0 -28
- package/src/router/response/openapi.test.ts +0 -96
- package/src/router/response/openapi.ts +0 -1
- package/src/router/response/serializers.ts +0 -66
- package/src/router/response/stream.ts +0 -35
- package/src/router/router.test.ts +0 -1571
- package/src/router/router.ts +0 -1965
- package/src/router/routes/index.ts +0 -46
- package/src/router/routes/valibot/index.ts +0 -18
- package/src/router/routes/valibot/valibot.ts +0 -1393
- package/src/router/routes/valibot.test.ts +0 -286
- package/src/router/routes/zod/index.ts +0 -18
- package/src/router/routes/zod/zod.ts +0 -1318
- package/src/router/routes/zod.test.ts +0 -280
- package/src/router/server-interface.ts +0 -31
- package/src/router/types.ts +0 -657
|
@@ -1,293 +0,0 @@
|
|
|
1
|
-
import { HttpMethod, StatusText } from '@mpen/http'
|
|
2
|
-
import type { HttpStatus } from '@mpen/http'
|
|
3
|
-
import type { Router } from '../../router'
|
|
4
|
-
import type { Handler, JsonObjectSchema, JsonSchema, NormalizedRoute, RouteMeta } from '../../types'
|
|
5
|
-
import { ok } from '../../response/formats/status'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* OpenAPI document `info` section.
|
|
9
|
-
*/
|
|
10
|
-
export type OpenApiInfo = {
|
|
11
|
-
title: string
|
|
12
|
-
version: string
|
|
13
|
-
description?: string
|
|
14
|
-
termsOfService?: string
|
|
15
|
-
contact?: Record<string, unknown>
|
|
16
|
-
license?: Record<string, unknown>
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* OpenAPI server definition.
|
|
21
|
-
*/
|
|
22
|
-
export type OpenApiServer = {
|
|
23
|
-
url: string
|
|
24
|
-
description?: string
|
|
25
|
-
variables?: Record<string, unknown>
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* OpenAPI operation object.
|
|
30
|
-
*/
|
|
31
|
-
export type OpenApiOperation = Record<string, unknown>
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* OpenAPI paths dictionary keyed by pathname then method.
|
|
35
|
-
*/
|
|
36
|
-
export type OpenApiPaths = Record<string, Record<string, OpenApiOperation>>
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* OpenAPI document returned by the `openapi` plugin handler.
|
|
40
|
-
*/
|
|
41
|
-
export type OpenApiDocument = {
|
|
42
|
-
openapi: string
|
|
43
|
-
info: OpenApiInfo
|
|
44
|
-
servers?: OpenApiServer[]
|
|
45
|
-
paths: OpenApiPaths
|
|
46
|
-
components?: Record<string, unknown>
|
|
47
|
-
security?: Array<Record<string, string[]>>
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Options used to build an OpenAPI document from registered routes.
|
|
52
|
-
*/
|
|
53
|
-
export type OpenApiOptions = {
|
|
54
|
-
info: OpenApiInfo
|
|
55
|
-
servers?: OpenApiServer[]
|
|
56
|
-
components?: Record<string, unknown>
|
|
57
|
-
security?: Array<Record<string, string[]>>
|
|
58
|
-
openapi?: string
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
type OpenApiParameter = {
|
|
62
|
-
name: string
|
|
63
|
-
in: 'path' | 'query'
|
|
64
|
-
required?: boolean
|
|
65
|
-
schema: JsonSchema
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
type OpenApiRequestBody = {
|
|
69
|
-
required?: boolean
|
|
70
|
-
content: Record<string, { schema: JsonSchema }>
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
type OpenApiResponse = {
|
|
74
|
-
description: string
|
|
75
|
-
content?: Record<string, { schema: JsonSchema }>
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const DEFAULT_OPENAPI_VERSION = '3.0.3'
|
|
79
|
-
const DEFAULT_METHODS: HttpMethod[] = [
|
|
80
|
-
HttpMethod.GET,
|
|
81
|
-
HttpMethod.PUT,
|
|
82
|
-
HttpMethod.POST,
|
|
83
|
-
HttpMethod.DELETE,
|
|
84
|
-
HttpMethod.OPTIONS,
|
|
85
|
-
HttpMethod.HEAD,
|
|
86
|
-
HttpMethod.PATCH,
|
|
87
|
-
HttpMethod.TRACE,
|
|
88
|
-
]
|
|
89
|
-
|
|
90
|
-
function routePathToOpenApi(pathname: string): string {
|
|
91
|
-
return pathname.replace(/:([A-Za-z0-9_]+)/g, '{$1}')
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function normalizeOpenApiMethods(route: NormalizedRoute<any>): string[] {
|
|
95
|
-
const rawMethods = route.method
|
|
96
|
-
? Array.isArray(route.method)
|
|
97
|
-
? route.method
|
|
98
|
-
: [route.method]
|
|
99
|
-
: DEFAULT_METHODS
|
|
100
|
-
const normalized = new Set<string>()
|
|
101
|
-
for (const method of rawMethods) {
|
|
102
|
-
if (method === HttpMethod.CONNECT) continue
|
|
103
|
-
normalized.add(method.toLowerCase())
|
|
104
|
-
}
|
|
105
|
-
return [...normalized]
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function buildParameterEntries(
|
|
109
|
-
schema: JsonObjectSchema,
|
|
110
|
-
location: 'path' | 'query',
|
|
111
|
-
): OpenApiParameter[] {
|
|
112
|
-
const properties = schema.properties
|
|
113
|
-
const requiredList = Array.isArray(schema.required)
|
|
114
|
-
? schema.required.filter((value): value is string => typeof value === 'string')
|
|
115
|
-
: []
|
|
116
|
-
if (properties && typeof properties === 'object') {
|
|
117
|
-
return Object.entries(properties).map(([name, propSchema]) => ({
|
|
118
|
-
name,
|
|
119
|
-
in: location,
|
|
120
|
-
required: location === 'path' ? true : requiredList.includes(name),
|
|
121
|
-
schema: (propSchema as JsonSchema) ?? {},
|
|
122
|
-
}))
|
|
123
|
-
}
|
|
124
|
-
return [
|
|
125
|
-
{
|
|
126
|
-
name: location,
|
|
127
|
-
in: location,
|
|
128
|
-
required: location === 'path',
|
|
129
|
-
schema,
|
|
130
|
-
},
|
|
131
|
-
]
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function openApiRequestContentTypes(route: NormalizedRoute<any>): string[] {
|
|
135
|
-
if (!route.accept || route.accept.length === 0) return ['application/json']
|
|
136
|
-
const normalized = new Set<string>()
|
|
137
|
-
for (const accept of route.accept) {
|
|
138
|
-
normalized.add(accept.type)
|
|
139
|
-
}
|
|
140
|
-
return [...normalized]
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function openApiResponseContentTypes(): string[] {
|
|
144
|
-
return ['application/json']
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function defaultResponseDescription(status: string): string {
|
|
148
|
-
const numericStatus = Number(status)
|
|
149
|
-
if (Number.isInteger(numericStatus)) {
|
|
150
|
-
return StatusText[numericStatus as HttpStatus] ?? String(status)
|
|
151
|
-
}
|
|
152
|
-
return status
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function buildOperationFromSchema(route: NormalizedRoute<any>): OpenApiOperation {
|
|
156
|
-
const schema = route.schema
|
|
157
|
-
const operation: OpenApiOperation = {}
|
|
158
|
-
if (!schema) {
|
|
159
|
-
operation.responses = { 200: { description: 'OK' } }
|
|
160
|
-
return operation
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const parameters: OpenApiParameter[] = []
|
|
164
|
-
if (schema.request?.query) {
|
|
165
|
-
parameters.push(...buildParameterEntries(schema.request.query, 'query'))
|
|
166
|
-
}
|
|
167
|
-
if (schema.request?.path) {
|
|
168
|
-
parameters.push(...buildParameterEntries(schema.request.path, 'path'))
|
|
169
|
-
}
|
|
170
|
-
if (parameters.length > 0) {
|
|
171
|
-
operation.parameters = parameters
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (schema.request?.body !== undefined) {
|
|
175
|
-
const content = Object.fromEntries(
|
|
176
|
-
openApiRequestContentTypes(route).map((contentType) => [
|
|
177
|
-
contentType,
|
|
178
|
-
{ schema: schema.request!.body! },
|
|
179
|
-
]),
|
|
180
|
-
)
|
|
181
|
-
operation.requestBody = {
|
|
182
|
-
required: true,
|
|
183
|
-
content,
|
|
184
|
-
} satisfies OpenApiRequestBody
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (schema.response?.body && Object.keys(schema.response.body).length > 0) {
|
|
188
|
-
const contentTypes = openApiResponseContentTypes()
|
|
189
|
-
operation.responses = Object.fromEntries(
|
|
190
|
-
Object.entries(schema.response.body).flatMap(([status, responseSchema]) => {
|
|
191
|
-
if (responseSchema === undefined) return []
|
|
192
|
-
const response: OpenApiResponse = {
|
|
193
|
-
description: defaultResponseDescription(status),
|
|
194
|
-
}
|
|
195
|
-
response.content = Object.fromEntries(
|
|
196
|
-
contentTypes.map((contentType) => [contentType, { schema: responseSchema }]),
|
|
197
|
-
)
|
|
198
|
-
return [[status, response]]
|
|
199
|
-
}),
|
|
200
|
-
)
|
|
201
|
-
} else {
|
|
202
|
-
operation.responses = { 200: { description: 'OK' } }
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return operation
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function mergeOpenApiOperations(
|
|
209
|
-
generated: OpenApiOperation,
|
|
210
|
-
custom?: RouteMeta['openapi'],
|
|
211
|
-
): OpenApiOperation {
|
|
212
|
-
if (!custom) return generated
|
|
213
|
-
|
|
214
|
-
const merged: OpenApiOperation = { ...generated, ...custom }
|
|
215
|
-
const generatedParameters = Array.isArray(generated.parameters) ? generated.parameters : []
|
|
216
|
-
const customParameters = Array.isArray(custom.parameters) ? custom.parameters : []
|
|
217
|
-
if (generatedParameters.length > 0 || customParameters.length > 0) {
|
|
218
|
-
merged.parameters = [...customParameters, ...generatedParameters]
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (generated.requestBody && custom.requestBody) {
|
|
222
|
-
const generatedRequestBody = generated.requestBody as OpenApiRequestBody
|
|
223
|
-
const customRequestBody = custom.requestBody as OpenApiRequestBody
|
|
224
|
-
merged.requestBody = {
|
|
225
|
-
...generatedRequestBody,
|
|
226
|
-
...customRequestBody,
|
|
227
|
-
content: {
|
|
228
|
-
...(generatedRequestBody.content ?? {}),
|
|
229
|
-
...(customRequestBody.content ?? {}),
|
|
230
|
-
},
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (generated.responses && custom.responses) {
|
|
235
|
-
merged.responses = {
|
|
236
|
-
...(generated.responses as Record<string, unknown>),
|
|
237
|
-
...(custom.responses as Record<string, unknown>),
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
return merged
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function buildOperation(route: NormalizedRoute<any>): OpenApiOperation {
|
|
245
|
-
return mergeOpenApiOperations(buildOperationFromSchema(route), route.meta?.openapi)
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Create an OpenAPI response handler that reflects the active router.
|
|
250
|
-
*
|
|
251
|
-
* @example
|
|
252
|
-
* ```ts
|
|
253
|
-
* router.add({
|
|
254
|
-
* path: '/swagger.json',
|
|
255
|
-
* method: HttpMethod.GET,
|
|
256
|
-
* handler: openapi({
|
|
257
|
-
* info: {title: 'Example API', version: '1.0.0'},
|
|
258
|
-
* servers: [{url: 'https://api.example.com'}],
|
|
259
|
-
* }),
|
|
260
|
-
* })
|
|
261
|
-
* ```
|
|
262
|
-
*
|
|
263
|
-
* @param options - OpenAPI document options for info, servers, and optional components/security.
|
|
264
|
-
* @returns A route handler that returns the generated OpenAPI JSON document.
|
|
265
|
-
*/
|
|
266
|
-
export function openapi(options: OpenApiOptions): Handler<OpenApiDocument> {
|
|
267
|
-
return function openapiHandler(this: Router<any>) {
|
|
268
|
-
const routes = this.getRoutes()
|
|
269
|
-
const paths: OpenApiPaths = {}
|
|
270
|
-
|
|
271
|
-
for (const route of routes) {
|
|
272
|
-
const pathPattern = routePathToOpenApi(route.path.pathname)
|
|
273
|
-
const methods = normalizeOpenApiMethods(route)
|
|
274
|
-
if (methods.length === 0) continue
|
|
275
|
-
|
|
276
|
-
const pathItem = paths[pathPattern] ?? (paths[pathPattern] = {})
|
|
277
|
-
for (const method of methods) {
|
|
278
|
-
pathItem[method] = buildOperation(route)
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const document: OpenApiDocument = {
|
|
283
|
-
openapi: options.openapi ?? DEFAULT_OPENAPI_VERSION,
|
|
284
|
-
info: options.info,
|
|
285
|
-
paths,
|
|
286
|
-
...(options.servers ? { servers: options.servers } : {}),
|
|
287
|
-
...(options.components ? { components: options.components } : {}),
|
|
288
|
-
...(options.security ? { security: options.security } : {}),
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return ok(document)
|
|
292
|
-
}
|
|
293
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env -S bun test
|
|
2
|
-
import { describe, expect, it } from 'bun:test'
|
|
3
|
-
import { HttpMethod } from '@mpen/http'
|
|
4
|
-
import { Router } from '../router'
|
|
5
|
-
import { openapi } from '../handlers/openapi'
|
|
6
|
-
import type { OpenApiDocument } from '../handlers/openapi'
|
|
7
|
-
import { createZodRouteBuilder } from '../routes/zod'
|
|
8
|
-
import { z } from 'zod'
|
|
9
|
-
|
|
10
|
-
type JsonSchemaObject = {
|
|
11
|
-
type?: string
|
|
12
|
-
properties: Record<string, JsonSchemaObject>
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
type OperationWithSchemas = {
|
|
16
|
-
requestBody: {
|
|
17
|
-
content: Record<string, { schema: JsonSchemaObject }>
|
|
18
|
-
}
|
|
19
|
-
responses: Record<string, { content: Record<string, { schema: JsonSchemaObject }> }>
|
|
20
|
-
parameters: unknown[]
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
describe('openapi', () => {
|
|
24
|
-
it('consumes route schemas generated by createZodRouteBuilder', async () => {
|
|
25
|
-
const route = createZodRouteBuilder()
|
|
26
|
-
const router = new Router()
|
|
27
|
-
router.add(
|
|
28
|
-
route({
|
|
29
|
-
path: '/users/:id',
|
|
30
|
-
method: HttpMethod.POST,
|
|
31
|
-
schema: {
|
|
32
|
-
request: {
|
|
33
|
-
path: z.object({ id: z.string() }),
|
|
34
|
-
query: z.object({ include: z.string() }),
|
|
35
|
-
body: z.object({ name: z.string() }),
|
|
36
|
-
},
|
|
37
|
-
response: {
|
|
38
|
-
body: {
|
|
39
|
-
200: z.object({ ok: z.boolean() }),
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
validateResponse: false,
|
|
44
|
-
handler: () => new Response('ok'),
|
|
45
|
-
}),
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
router.add({
|
|
49
|
-
path: '/swagger.json',
|
|
50
|
-
method: HttpMethod.GET,
|
|
51
|
-
handler: openapi({
|
|
52
|
-
info: { title: 'Example API', version: '1.0.0' },
|
|
53
|
-
}),
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
const response = await router.fetch(new Request('https://example.com/swagger.json'))
|
|
57
|
-
const document = (await response.json()) as OpenApiDocument
|
|
58
|
-
const operation = document.paths['/users/{id}'].post as OperationWithSchemas
|
|
59
|
-
|
|
60
|
-
expect(operation.requestBody.content['application/json'].schema.type).toBe('object')
|
|
61
|
-
expect(operation.requestBody.content['application/json'].schema.properties.name.type).toBe(
|
|
62
|
-
'string',
|
|
63
|
-
)
|
|
64
|
-
expect(
|
|
65
|
-
operation.responses['200'].content['application/json'].schema.properties.ok.type,
|
|
66
|
-
).toBe('boolean')
|
|
67
|
-
expect(operation.parameters).toEqual(
|
|
68
|
-
expect.arrayContaining([
|
|
69
|
-
expect.objectContaining({ name: 'id', in: 'path' }),
|
|
70
|
-
expect.objectContaining({ name: 'include', in: 'query' }),
|
|
71
|
-
]),
|
|
72
|
-
)
|
|
73
|
-
})
|
|
74
|
-
})
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env -S bun test
|
|
2
|
-
import { describe, expect, it } from 'bun:test'
|
|
3
|
-
import { normalizeCharsetName } from './charset'
|
|
4
|
-
|
|
5
|
-
describe(normalizeCharsetName.name, () => {
|
|
6
|
-
it('returns preferred MIME names for common aliases', () => {
|
|
7
|
-
expect(normalizeCharsetName('UTF8')).toBe('utf-8')
|
|
8
|
-
expect(normalizeCharsetName('latin1')).toBe('iso-8859-1')
|
|
9
|
-
expect(normalizeCharsetName('ANSI_X3.4-1968')).toBe('us-ascii')
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
it('ignores punctuation and whitespace differences', () => {
|
|
13
|
-
expect(normalizeCharsetName(' utf_16 ')).toBe('utf-16')
|
|
14
|
-
expect(normalizeCharsetName('utf 8')).toBe('utf-8')
|
|
15
|
-
expect(normalizeCharsetName('Shift-JIS')).toBe('shift_jis')
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it('returns an empty string for missing input', () => {
|
|
19
|
-
expect(normalizeCharsetName('')).toBe('')
|
|
20
|
-
expect(normalizeCharsetName(' ')).toBe('')
|
|
21
|
-
})
|
|
22
|
-
})
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Normalize a charset label to a lowercase "Preferred MIME Name" where possible.
|
|
3
|
-
*
|
|
4
|
-
* - Charset comparison is case-insensitive (IANA).
|
|
5
|
-
* - Uses Preferred MIME Names when mapped.
|
|
6
|
-
* - Unknown charsets return a cleaned lowercase form.
|
|
7
|
-
*/
|
|
8
|
-
export function normalizeCharsetName(input: string): string {
|
|
9
|
-
if (!input?.length) return ''
|
|
10
|
-
|
|
11
|
-
const raw = input.trim()
|
|
12
|
-
if (raw.length === 0) return ''
|
|
13
|
-
|
|
14
|
-
const keyA = normalizeKey(raw)
|
|
15
|
-
const hitA = ALIAS_TO_PREFERRED_LOWER.get(keyA)
|
|
16
|
-
if (hitA) return hitA
|
|
17
|
-
|
|
18
|
-
// Secondary fuzzy match: ignore punctuation differences (utf8 vs utf-8)
|
|
19
|
-
const keyB = stripKey(keyA)
|
|
20
|
-
const hitB = STRIPPED_ALIAS_TO_PREFERRED_LOWER.get(keyB)
|
|
21
|
-
if (hitB) return hitB
|
|
22
|
-
|
|
23
|
-
// Fallback: normalized lowercase token
|
|
24
|
-
return keyA
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function normalizeKey(s: string): string {
|
|
28
|
-
// IANA says comparison is case-insensitive; we normalize to lowercase. :contentReference[oaicite:4]{index=4}
|
|
29
|
-
return s
|
|
30
|
-
.trim()
|
|
31
|
-
.toLowerCase()
|
|
32
|
-
.replace(/\s+/g, '') // drop internal spaces
|
|
33
|
-
.replace(/_/g, '-') // common alias form
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function stripKey(s: string): string {
|
|
37
|
-
// Keep only alnum for fuzzy matching across punctuation variants.
|
|
38
|
-
return s.replace(/[^a-z0-9]+/g, '')
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Minimal, practical alias set.
|
|
43
|
-
* Extend this map as needed (ideally generated from IANA CSV for full coverage). :contentReference[oaicite:5]{index=5}
|
|
44
|
-
*/
|
|
45
|
-
const ALIAS_TO_PREFERRED_LOWER = new Map<string, string>([
|
|
46
|
-
// Unicode
|
|
47
|
-
['utf-8', 'utf-8'],
|
|
48
|
-
['utf8', 'utf-8'],
|
|
49
|
-
['unicode-1-1-utf-8', 'utf-8'],
|
|
50
|
-
|
|
51
|
-
['utf-16', 'utf-16'],
|
|
52
|
-
['utf16', 'utf-16'],
|
|
53
|
-
['utf-16le', 'utf-16le'],
|
|
54
|
-
['utf-16be', 'utf-16be'],
|
|
55
|
-
|
|
56
|
-
// ASCII
|
|
57
|
-
['us-ascii', 'us-ascii'],
|
|
58
|
-
['ascii', 'us-ascii'],
|
|
59
|
-
['ansi_x3.4-1968', 'us-ascii'],
|
|
60
|
-
['ansi_x3.4-1986', 'us-ascii'],
|
|
61
|
-
['iso646-us', 'us-ascii'],
|
|
62
|
-
['cp367', 'us-ascii'],
|
|
63
|
-
['ibm367', 'us-ascii'],
|
|
64
|
-
|
|
65
|
-
// ISO-8859 (Latin / Cyrillic / etc.)
|
|
66
|
-
['iso-8859-1', 'iso-8859-1'],
|
|
67
|
-
['iso_8859-1:1987', 'iso-8859-1'],
|
|
68
|
-
['iso_8859-1', 'iso-8859-1'],
|
|
69
|
-
['latin1', 'iso-8859-1'],
|
|
70
|
-
['l1', 'iso-8859-1'],
|
|
71
|
-
['cp819', 'iso-8859-1'],
|
|
72
|
-
['ibm819', 'iso-8859-1'],
|
|
73
|
-
|
|
74
|
-
['iso-8859-2', 'iso-8859-2'],
|
|
75
|
-
['latin2', 'iso-8859-2'],
|
|
76
|
-
['l2', 'iso-8859-2'],
|
|
77
|
-
|
|
78
|
-
['iso-8859-3', 'iso-8859-3'],
|
|
79
|
-
['latin3', 'iso-8859-3'],
|
|
80
|
-
['l3', 'iso-8859-3'],
|
|
81
|
-
|
|
82
|
-
['iso-8859-4', 'iso-8859-4'],
|
|
83
|
-
['latin4', 'iso-8859-4'],
|
|
84
|
-
['l4', 'iso-8859-4'],
|
|
85
|
-
|
|
86
|
-
['iso-8859-5', 'iso-8859-5'],
|
|
87
|
-
['cyrillic', 'iso-8859-5'],
|
|
88
|
-
|
|
89
|
-
['iso-8859-6', 'iso-8859-6'],
|
|
90
|
-
['arabic', 'iso-8859-6'],
|
|
91
|
-
|
|
92
|
-
['iso-8859-7', 'iso-8859-7'],
|
|
93
|
-
['greek', 'iso-8859-7'],
|
|
94
|
-
['greek8', 'iso-8859-7'],
|
|
95
|
-
|
|
96
|
-
['iso-8859-8', 'iso-8859-8'],
|
|
97
|
-
['hebrew', 'iso-8859-8'],
|
|
98
|
-
|
|
99
|
-
['iso-8859-9', 'iso-8859-9'],
|
|
100
|
-
['latin5', 'iso-8859-9'],
|
|
101
|
-
['l5', 'iso-8859-9'],
|
|
102
|
-
|
|
103
|
-
// Common “windows” encodings
|
|
104
|
-
['windows-1252', 'windows-1252'],
|
|
105
|
-
['cp1252', 'windows-1252'],
|
|
106
|
-
|
|
107
|
-
['windows-1251', 'windows-1251'],
|
|
108
|
-
['cp1251', 'windows-1251'],
|
|
109
|
-
|
|
110
|
-
// East Asian (common on the web)
|
|
111
|
-
['shift_jis', 'shift_jis'],
|
|
112
|
-
['shift-jis', 'shift_jis'],
|
|
113
|
-
['sjis', 'shift_jis'],
|
|
114
|
-
['ms_kanji', 'shift_jis'],
|
|
115
|
-
|
|
116
|
-
['euc-jp', 'euc-jp'],
|
|
117
|
-
['eucjp', 'euc-jp'],
|
|
118
|
-
|
|
119
|
-
['euc-kr', 'euc-kr'],
|
|
120
|
-
['euckr', 'euc-kr'],
|
|
121
|
-
|
|
122
|
-
['iso-2022-jp', 'iso-2022-jp'],
|
|
123
|
-
['iso-2022-kr', 'iso-2022-kr'],
|
|
124
|
-
|
|
125
|
-
['big5', 'big5'],
|
|
126
|
-
|
|
127
|
-
['gbk', 'gbk'],
|
|
128
|
-
['gb18030', 'gb18030'],
|
|
129
|
-
])
|
|
130
|
-
|
|
131
|
-
const STRIPPED_ALIAS_TO_PREFERRED_LOWER = new Map<string, string>(
|
|
132
|
-
Array.from(ALIAS_TO_PREFERRED_LOWER.entries(), ([k, v]) => [stripKey(k), v]),
|
|
133
|
-
)
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env -S bun test
|
|
2
|
-
import { describe, expect, it } from 'bun:test'
|
|
3
|
-
import { fullWide } from './format'
|
|
4
|
-
|
|
5
|
-
describe(fullWide.name, () => {
|
|
6
|
-
it('formats finite numbers without grouping or exponential notation', () => {
|
|
7
|
-
expect(fullWide(1234567)).toBe('1234567')
|
|
8
|
-
expect(fullWide(1e21)).toBe('1000000000000000000000')
|
|
9
|
-
expect(fullWide(1234.5)).toBe('1234.5')
|
|
10
|
-
expect(fullWide(0.0000001)).toBe('0.0000001')
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
it('formats bigint values as base-10 strings', () => {
|
|
14
|
-
expect(fullWide(123_456_789_012_345_678_901_234_567_890n)).toBe(
|
|
15
|
-
'123456789012345678901234567890',
|
|
16
|
-
)
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it('trims leading zeroes', () => {
|
|
20
|
-
expect(fullWide('000123')).toBe('123')
|
|
21
|
-
expect(fullWide('+000123')).toBe('123')
|
|
22
|
-
expect(fullWide('-000123')).toBe('-123')
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
it('trims trailing zeros zeroes', () => {
|
|
26
|
-
expect(fullWide('1230')).toBe('1230')
|
|
27
|
-
expect(fullWide('1230.')).toBe('1230')
|
|
28
|
-
expect(fullWide('1230.0')).toBe('1230')
|
|
29
|
-
expect(fullWide('1230.01')).toBe('1230.01')
|
|
30
|
-
expect(fullWide('1230.010')).toBe('1230.01')
|
|
31
|
-
expect(fullWide('1230.010200')).toBe('1230.0102')
|
|
32
|
-
|
|
33
|
-
expect(fullWide('+1230')).toBe('1230')
|
|
34
|
-
expect(fullWide('+1230.')).toBe('1230')
|
|
35
|
-
expect(fullWide('+1230.0')).toBe('1230')
|
|
36
|
-
expect(fullWide('+1230.01')).toBe('1230.01')
|
|
37
|
-
expect(fullWide('+1230.010')).toBe('1230.01')
|
|
38
|
-
|
|
39
|
-
expect(fullWide('-1230')).toBe('-1230')
|
|
40
|
-
expect(fullWide('-1230.')).toBe('-1230')
|
|
41
|
-
expect(fullWide('-1230.0')).toBe('-1230')
|
|
42
|
-
expect(fullWide('-1230.01')).toBe('-1230.01')
|
|
43
|
-
expect(fullWide('-1230.010')).toBe('-1230.01')
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('formats decimal strings without losing precision', () => {
|
|
47
|
-
expect(fullWide('0.000000000000000001')).toBe('0.000000000000000001')
|
|
48
|
-
expect(fullWide('123456789012345678901234567890.125')).toBe(
|
|
49
|
-
'123456789012345678901234567890.125',
|
|
50
|
-
)
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('returns safe integer bounds for infinite numeric values', () => {
|
|
54
|
-
expect(fullWide(Number.POSITIVE_INFINITY)).toBe(String(Number.MAX_SAFE_INTEGER))
|
|
55
|
-
expect(fullWide(Number.NEGATIVE_INFINITY)).toBe(String(Number.MIN_SAFE_INTEGER))
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('returns zero for NaN numeric values', () => {
|
|
59
|
-
expect(fullWide(Number.NaN)).toBe('0')
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it('returns safe integer bounds for infinite string values', () => {
|
|
63
|
-
expect(fullWide('Infinity')).toBe(String(Number.MAX_SAFE_INTEGER))
|
|
64
|
-
expect(fullWide('+Infinity')).toBe(String(Number.MAX_SAFE_INTEGER))
|
|
65
|
-
expect(fullWide('-Infinity')).toBe(String(Number.MIN_SAFE_INTEGER))
|
|
66
|
-
})
|
|
67
|
-
})
|
package/src/router/lib/format.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
export type FormattableNumber = Parameters<Intl.NumberFormat['format']>[0]
|
|
2
|
-
|
|
3
|
-
const FULL_WIDE_FORMAT = new Intl.NumberFormat('en-US', {
|
|
4
|
-
useGrouping: false,
|
|
5
|
-
maximumFractionDigits: 20,
|
|
6
|
-
})
|
|
7
|
-
|
|
8
|
-
const DECIMAL_STRING = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/
|
|
9
|
-
const MAX_SAFE_INTEGER_STRING = String(Number.MAX_SAFE_INTEGER)
|
|
10
|
-
const MIN_SAFE_INTEGER_STRING = String(Number.MIN_SAFE_INTEGER)
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Formats a number with full decimal places.
|
|
14
|
-
*
|
|
15
|
-
* e.g. `1e21` formats as "1000000000000000000000" instead of "1e+21"
|
|
16
|
-
*
|
|
17
|
-
* @param n The number to format.
|
|
18
|
-
*/
|
|
19
|
-
export function fullWide(n: FormattableNumber): string {
|
|
20
|
-
if (typeof n === 'bigint') return n.toString()
|
|
21
|
-
|
|
22
|
-
if (typeof n === 'string') {
|
|
23
|
-
if (n === 'Infinity' || n === '+Infinity') return MAX_SAFE_INTEGER_STRING
|
|
24
|
-
if (n === '-Infinity') return MIN_SAFE_INTEGER_STRING
|
|
25
|
-
if (!DECIMAL_STRING.test(n)) return '0'
|
|
26
|
-
|
|
27
|
-
return FULL_WIDE_FORMAT.format(n)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (n === Number.POSITIVE_INFINITY) return MAX_SAFE_INTEGER_STRING
|
|
31
|
-
if (n === Number.NEGATIVE_INFINITY) return MIN_SAFE_INTEGER_STRING
|
|
32
|
-
if (!Number.isFinite(n)) return '0'
|
|
33
|
-
|
|
34
|
-
return FULL_WIDE_FORMAT.format(n)
|
|
35
|
-
}
|
package/src/router/lib/host.ts
DELETED