@kamaalio/hono-standard-openapi 0.0.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/LICENSE +21 -0
- package/README.md +74 -0
- package/dist/app.d.ts +44 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +169 -0
- package/dist/app.js.map +1 -0
- package/dist/errors.d.ts +17 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +29 -0
- package/dist/errors.js.map +1 -0
- package/dist/generator.d.ts +25 -0
- package/dist/generator.d.ts.map +1 -0
- package/dist/generator.js +231 -0
- package/dist/generator.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/json-schema.d.ts +65 -0
- package/dist/json-schema.d.ts.map +1 -0
- package/dist/json-schema.js +248 -0
- package/dist/json-schema.js.map +1 -0
- package/dist/registry.d.ts +44 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +78 -0
- package/dist/registry.js.map +1 -0
- package/dist/route.d.ts +24 -0
- package/dist/route.d.ts.map +1 -0
- package/dist/route.js +19 -0
- package/dist/route.js.map +1 -0
- package/dist/standard-schema.d.ts +17 -0
- package/dist/standard-schema.d.ts.map +1 -0
- package/dist/standard-schema.js +16 -0
- package/dist/standard-schema.js.map +1 -0
- package/dist/type-inference.d.ts +80 -0
- package/dist/type-inference.d.ts.map +1 -0
- package/dist/type-inference.js +2 -0
- package/dist/type-inference.js.map +1 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/validator.d.ts +30 -0
- package/dist/validator.d.ts.map +1 -0
- package/dist/validator.js +31 -0
- package/dist/validator.js.map +1 -0
- package/package.json +73 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kamaal Farah
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# hono-standard-openapi
|
|
2
|
+
|
|
3
|
+
Generate an OpenAPI document from a Hono app whose routes are described by
|
|
4
|
+
[Standard Schema](https://standardschema.dev) and
|
|
5
|
+
[Standard JSON Schema](https://standardschema.dev/json-schema).
|
|
6
|
+
|
|
7
|
+
The package is schema-library neutral: it consumes the validation and JSON Schema interfaces defined
|
|
8
|
+
by those specifications. Use your chosen implementation's normal mechanism to add JSON Schema
|
|
9
|
+
metadata, and the document follows.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { StandardOpenAPIHono, createRoute, type StandardSchema } from 'hono-standard-openapi';
|
|
13
|
+
|
|
14
|
+
// Define these with any Standard Schema-compatible implementation.
|
|
15
|
+
declare const CardSchema: StandardSchema;
|
|
16
|
+
declare const CardParamsSchema: StandardSchema;
|
|
17
|
+
|
|
18
|
+
const route = createRoute({
|
|
19
|
+
method: 'get',
|
|
20
|
+
path: '/cards/{cardId}',
|
|
21
|
+
request: { params: CardParamsSchema },
|
|
22
|
+
responses: {
|
|
23
|
+
200: { description: 'The card', content: { 'application/json': { schema: CardSchema } } },
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const app = new StandardOpenAPIHono();
|
|
28
|
+
app.openapi(route, c => c.json(findCard(c.req.valid('param').cardId)));
|
|
29
|
+
app.doc('/spec.json', { openapi: '3.1.1', info: { title: 'Cards', version: '1.0.0' } });
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`c.req.valid('param')` is typed from the schema, the request is validated before the handler runs,
|
|
33
|
+
and `/spec.json` describes the route — path parameters, response body, and a `Card` component the
|
|
34
|
+
response `$ref`s.
|
|
35
|
+
|
|
36
|
+
## Naming components
|
|
37
|
+
|
|
38
|
+
A schema becomes a named component by carrying `$id`. Anything without one is described inline.
|
|
39
|
+
|
|
40
|
+
`$id` is an ordinary JSON Schema keyword, so it can name a component wherever the schema appears,
|
|
41
|
+
including as a response root. See [docs/design.md](./docs/design.md) for the details.
|
|
42
|
+
|
|
43
|
+
Schemas that carry neither can be named from the outside:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
app.openAPIRegistry.register('Card', CardSchema);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Validation
|
|
50
|
+
|
|
51
|
+
Failures reach the route's hook, or the nearest `defaultHook` on the app or the app it is mounted
|
|
52
|
+
under, and otherwise answer `400`:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const app = new StandardOpenAPIHono({
|
|
56
|
+
defaultHook: (result, c) => {
|
|
57
|
+
if (!result.success) throw new InvalidRequest(c, result.error.issues);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`result.error.issues` is Standard Schema's issue list — `{ message, path? }` — plus whatever else the
|
|
63
|
+
schema library reports on each issue.
|
|
64
|
+
|
|
65
|
+
## Normalization
|
|
66
|
+
|
|
67
|
+
Converted JSON Schema is adjusted to read as an idiomatic OpenAPI document: the strictness marker
|
|
68
|
+
from stripping objects is dropped, a `pattern` implied by a `format` is dropped, a nullable union
|
|
69
|
+
becomes a nullable type, `const` becomes a single-value `enum`, and keywords are emitted in a
|
|
70
|
+
conventional order. Each is switchable:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
app.getOpenAPIDocument(config, { normalization: { constToEnum: false } });
|
|
74
|
+
```
|
package/dist/app.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type Env, Hono, type Schema } from 'hono';
|
|
2
|
+
import type { OpenAPIObject } from 'openapi3-ts/oas31';
|
|
3
|
+
import { type DocumentConfig, type GeneratorOptions } from './generator.ts';
|
|
4
|
+
import { OpenAPIRegistry } from './registry.ts';
|
|
5
|
+
import type { RouteConfig } from './route.ts';
|
|
6
|
+
import type { RouteHandler } from './type-inference.ts';
|
|
7
|
+
import { PARAMETER_SOURCES } from './types.ts';
|
|
8
|
+
import { type Hook } from './validator.ts';
|
|
9
|
+
export interface StandardOpenAPIHonoOptions<E extends Env> {
|
|
10
|
+
/** Runs for every validation on this app, and on apps mounted under it that define none. */
|
|
11
|
+
readonly defaultHook?: Hook<unknown, E, string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
type HonoInit<E extends Env> = ConstructorParameters<typeof Hono>[0] & StandardOpenAPIHonoOptions<E>;
|
|
14
|
+
/**
|
|
15
|
+
* A Hono app that documents itself.
|
|
16
|
+
*
|
|
17
|
+
* Routes registered through {@link StandardOpenAPIHono.openapi} are both served and recorded, so the
|
|
18
|
+
* document can never drift from what the server actually accepts.
|
|
19
|
+
*/
|
|
20
|
+
export declare class StandardOpenAPIHono<E extends Env = Env, S extends Schema = {}, BasePath extends string = '/'> extends Hono<E, S, BasePath> {
|
|
21
|
+
#private;
|
|
22
|
+
readonly openAPIRegistry: OpenAPIRegistry;
|
|
23
|
+
readonly defaultHook: StandardOpenAPIHonoOptions<E>['defaultHook'];
|
|
24
|
+
constructor(init?: HonoInit<E>);
|
|
25
|
+
/** Registers a route: mounts it, validates its request, and records it in the document. */
|
|
26
|
+
openapi<R extends RouteConfig>(route: R, handler: RouteHandler<R, E>, hook?: Hook<unknown, E, string, unknown>): this;
|
|
27
|
+
/** Mounts another app, taking its documented routes along with its handlers. */
|
|
28
|
+
route<SubPath extends string, SubEnv extends Env, SubSchema extends Schema, SubBasePath extends string>(path: SubPath, app: Hono<SubEnv, SubSchema, SubBasePath>): this;
|
|
29
|
+
/** Builds the document for everything registered so far. */
|
|
30
|
+
getOpenAPIDocument(config: DocumentConfig, generatorConfig?: GeneratorOptions): OpenAPIObject;
|
|
31
|
+
/** Serves the document as JSON at `path`. */
|
|
32
|
+
doc(path: string, config: DocumentConfig, generatorConfig?: GeneratorOptions): this;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The documenting app type behind a plain Hono type.
|
|
36
|
+
*
|
|
37
|
+
* Hono's own chaining methods return `Hono`, which loses the registry from the type but not from the
|
|
38
|
+
* value; this names what the value actually is.
|
|
39
|
+
*/
|
|
40
|
+
export type HonoToStandardOpenAPIHono<T> = T extends Hono<infer E, infer S, infer BasePath> ? StandardOpenAPIHono<E, S, BasePath> : T;
|
|
41
|
+
/** Restores an app's type after Hono's own chaining methods widen it. */
|
|
42
|
+
export declare function $<T extends Hono<any, any, any>>(app: T): HonoToStandardOpenAPIHono<T>;
|
|
43
|
+
export { PARAMETER_SOURCES };
|
|
44
|
+
//# sourceMappingURL=app.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,GAAG,EAAE,IAAI,EAA0B,KAAK,MAAM,EAA0B,MAAM,MAAM,CAAC;AAEnG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,gBAAgB,EAAoB,MAAM,gBAAgB,CAAC;AAE9F,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAsB,iBAAiB,EAAqB,MAAM,YAAY,CAAC;AACtF,OAAO,EAAE,KAAK,IAAI,EAAqB,MAAM,gBAAgB,CAAC;AAE9D,MAAM,WAAW,0BAA0B,CAAC,CAAC,SAAS,GAAG;IACvD,4FAA4F;IAC5F,QAAQ,CAAC,WAAW,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;CAC1D;AAED,KAAK,QAAQ,CAAC,CAAC,SAAS,GAAG,IAAI,qBAAqB,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,0BAA0B,CAAC,CAAC,CAAC,CAAC;AAUrG;;;;;GAKG;AACH,qBAAa,mBAAmB,CAC9B,CAAC,SAAS,GAAG,GAAG,GAAG,EACnB,CAAC,SAAS,MAAM,GAAG,EAAE,EACrB,QAAQ,SAAS,MAAM,GAAG,GAAG,CAC7B,SAAQ,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC;;IAC5B,QAAQ,CAAC,eAAe,EAAE,eAAe,CAAC;IAC1C,QAAQ,CAAC,WAAW,EAAE,0BAA0B,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC;IAGnE,YAAY,IAAI,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,EAI7B;IAED,2FAA2F;IAC3F,OAAO,CAAC,CAAC,SAAS,WAAW,EAC3B,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,EAC3B,IAAI,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,GACvC,IAAI,CAmBN;IAED,gFAAgF;IAChF,KAAK,CAAC,OAAO,SAAS,MAAM,EAAE,MAAM,SAAS,GAAG,EAAE,SAAS,SAAS,MAAM,EAAE,WAAW,SAAS,MAAM,EACpG,IAAI,EAAE,OAAO,EACb,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,WAAW,CAAC,GACxC,IAAI,CAUN;IAED,4DAA4D;IAC5D,kBAAkB,CAAC,MAAM,EAAE,cAAc,EAAE,eAAe,GAAE,gBAAqB,GAAG,aAAa,CAEhG;IAED,6CAA6C;IAC7C,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,eAAe,GAAE,gBAAqB,GAAG,IAAI,CAItF;CAsCF;AAwFD;;;;;GAKG;AACH,MAAM,MAAM,yBAAyB,CAAC,CAAC,IACrC,CAAC,SAAS,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,QAAQ,CAAC,GAAG,mBAAmB,CAAC,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC;AAE7F,yEAAyE;AAEzE,wBAAgB,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,yBAAyB,CAAC,CAAC,CAAC,CAGrF;AAED,OAAO,EAAE,iBAAiB,EAAE,CAAC"}
|
package/dist/app.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { mergePath } from 'hono/utils/url';
|
|
3
|
+
import { OpenAPIGenerator } from './generator.js';
|
|
4
|
+
import { ComponentCollector, convertSchema } from './json-schema.js';
|
|
5
|
+
import { OpenAPIRegistry } from './registry.js';
|
|
6
|
+
import { isStandardJSONSchema } from './standard-schema.js';
|
|
7
|
+
import { PARAMETER_SOURCES } from './types.js';
|
|
8
|
+
import { standardValidator } from './validator.js';
|
|
9
|
+
const JSON_CONTENT_TYPE = /^application\/([a-z\-.]+\+)?json/;
|
|
10
|
+
const FORM_CONTENT_TYPES = ['multipart/form-data', 'application/x-www-form-urlencoded'];
|
|
11
|
+
/**
|
|
12
|
+
* A Hono app that documents itself.
|
|
13
|
+
*
|
|
14
|
+
* Routes registered through {@link StandardOpenAPIHono.openapi} are both served and recorded, so the
|
|
15
|
+
* document can never drift from what the server actually accepts.
|
|
16
|
+
*/
|
|
17
|
+
export class StandardOpenAPIHono extends Hono {
|
|
18
|
+
openAPIRegistry;
|
|
19
|
+
defaultHook;
|
|
20
|
+
#parentApp;
|
|
21
|
+
constructor(init) {
|
|
22
|
+
super(init);
|
|
23
|
+
this.openAPIRegistry = new OpenAPIRegistry();
|
|
24
|
+
this.defaultHook = init?.defaultHook;
|
|
25
|
+
}
|
|
26
|
+
/** Registers a route: mounts it, validates its request, and records it in the document. */
|
|
27
|
+
openapi(route, handler, hook) {
|
|
28
|
+
const { hide, middleware, ...documented } = route;
|
|
29
|
+
if (hide !== true)
|
|
30
|
+
this.openAPIRegistry.registerPath(documented);
|
|
31
|
+
const effectiveHook = (result, c) => {
|
|
32
|
+
const resolved = hook ?? this.#resolveDefaultHook();
|
|
33
|
+
return resolved?.(result, c);
|
|
34
|
+
};
|
|
35
|
+
const handlers = [
|
|
36
|
+
...normalizeMiddleware(middleware),
|
|
37
|
+
...this.#buildValidators(route.request, effectiveHook),
|
|
38
|
+
handler,
|
|
39
|
+
];
|
|
40
|
+
// @ts-expect-error: the handler chain is validated by `RouteHandler`, not by Hono's own inference.
|
|
41
|
+
this.on([route.method], [toRoutingPath(route.path)], ...handlers);
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
/** Mounts another app, taking its documented routes along with its handlers. */
|
|
45
|
+
route(path, app) {
|
|
46
|
+
super.route(path, app);
|
|
47
|
+
if (app instanceof StandardOpenAPIHono) {
|
|
48
|
+
app.#parentApp ??= this;
|
|
49
|
+
const prefix = path.replaceAll(/:([^/]+)/g, '{$1}');
|
|
50
|
+
this.openAPIRegistry.absorb(app.openAPIRegistry, routePath => mergePath(prefix, routePath));
|
|
51
|
+
}
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
/** Builds the document for everything registered so far. */
|
|
55
|
+
getOpenAPIDocument(config, generatorConfig = {}) {
|
|
56
|
+
return new OpenAPIGenerator(this.openAPIRegistry, generatorConfig).generateDocument(config);
|
|
57
|
+
}
|
|
58
|
+
/** Serves the document as JSON at `path`. */
|
|
59
|
+
doc(path, config, generatorConfig = {}) {
|
|
60
|
+
this.get(path, c => c.json(this.getOpenAPIDocument(config, generatorConfig)));
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
/** The nearest hook, preferring this app's own and falling back to the app it is mounted under. */
|
|
64
|
+
#resolveDefaultHook() {
|
|
65
|
+
if (this.defaultHook != null)
|
|
66
|
+
return this.defaultHook;
|
|
67
|
+
const visited = new Set([this]);
|
|
68
|
+
let ancestor = this.#parentApp;
|
|
69
|
+
while (ancestor != null && !visited.has(ancestor)) {
|
|
70
|
+
if (ancestor.defaultHook != null)
|
|
71
|
+
return ancestor.defaultHook;
|
|
72
|
+
visited.add(ancestor);
|
|
73
|
+
ancestor = ancestor.#parentApp;
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
#buildValidators(request, hook) {
|
|
78
|
+
if (request == null)
|
|
79
|
+
return [];
|
|
80
|
+
const validators = [];
|
|
81
|
+
for (const { key, target } of VALIDATED_PARTS) {
|
|
82
|
+
const schema = request[key];
|
|
83
|
+
if (schema == null)
|
|
84
|
+
continue;
|
|
85
|
+
validators.push(standardValidator(target, schema, propertyNamesOf(schema), hook));
|
|
86
|
+
}
|
|
87
|
+
const body = request.body;
|
|
88
|
+
if (body != null)
|
|
89
|
+
validators.push(...buildBodyValidators(body.content, body.required === true, hook));
|
|
90
|
+
return validators;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const VALIDATED_PARTS = [
|
|
94
|
+
{ key: 'query', target: 'query' },
|
|
95
|
+
{ key: 'params', target: 'param' },
|
|
96
|
+
{ key: 'headers', target: 'header' },
|
|
97
|
+
{ key: 'cookies', target: 'cookie' },
|
|
98
|
+
];
|
|
99
|
+
function buildBodyValidators(content, required, hook) {
|
|
100
|
+
const validators = [];
|
|
101
|
+
for (const [mediaType, media] of Object.entries(content)) {
|
|
102
|
+
const schema = media.schema;
|
|
103
|
+
if (!isStandardJSONSchema(schema))
|
|
104
|
+
continue;
|
|
105
|
+
const target = JSON_CONTENT_TYPE.test(mediaType)
|
|
106
|
+
? 'json'
|
|
107
|
+
: FORM_CONTENT_TYPES.some(formType => mediaType.startsWith(formType))
|
|
108
|
+
? 'form'
|
|
109
|
+
: undefined;
|
|
110
|
+
if (target == null)
|
|
111
|
+
continue;
|
|
112
|
+
const validator = standardValidator(target, schema, propertyNamesOf(schema), hook);
|
|
113
|
+
validators.push(required ? validator : skipWhenBodyAbsent(validator, mediaType, target));
|
|
114
|
+
}
|
|
115
|
+
return validators;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Lets an optional body through when the request didn't send one.
|
|
119
|
+
*
|
|
120
|
+
* The handler still gets an empty value from `c.req.valid()`, so it can read the body the same way
|
|
121
|
+
* whether or not the caller supplied it.
|
|
122
|
+
*/
|
|
123
|
+
function skipWhenBodyAbsent(validator, mediaType, target) {
|
|
124
|
+
return async (c, next) => {
|
|
125
|
+
const contentType = c.req.header('content-type');
|
|
126
|
+
if (contentType != null && contentType.startsWith(mediaType.replace(/;.*/, ''))) {
|
|
127
|
+
return validator(c, next);
|
|
128
|
+
}
|
|
129
|
+
c.req.addValidatedData(target, {});
|
|
130
|
+
await next();
|
|
131
|
+
return undefined;
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** The property names a schema describes, used to match header names case-insensitively. */
|
|
135
|
+
function propertyNamesOf(schema) {
|
|
136
|
+
try {
|
|
137
|
+
const converted = convertSchema(schema, {
|
|
138
|
+
components: new ComponentCollector(),
|
|
139
|
+
hoistRoot: false,
|
|
140
|
+
io: 'input',
|
|
141
|
+
target: 'draft-2020-12',
|
|
142
|
+
});
|
|
143
|
+
const properties = converted.properties;
|
|
144
|
+
if (typeof properties !== 'object' || properties == null)
|
|
145
|
+
return [];
|
|
146
|
+
return Object.keys(properties);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function normalizeMiddleware(middleware) {
|
|
153
|
+
if (middleware == null)
|
|
154
|
+
return [];
|
|
155
|
+
if (typeof middleware === 'function')
|
|
156
|
+
return [middleware];
|
|
157
|
+
return [...middleware];
|
|
158
|
+
}
|
|
159
|
+
function toRoutingPath(path) {
|
|
160
|
+
return path.replaceAll(/\/{(.+?)}/g, '/:$1');
|
|
161
|
+
}
|
|
162
|
+
/** Restores an app's type after Hono's own chaining methods widen it. */
|
|
163
|
+
// oxlint-disable-next-line typescript/no-explicit-any
|
|
164
|
+
export function $(app) {
|
|
165
|
+
// @ts-expect-error: chaining only widens the type; the value is still this package's app.
|
|
166
|
+
return app;
|
|
167
|
+
}
|
|
168
|
+
export { PARAMETER_SOURCES };
|
|
169
|
+
//# sourceMappingURL=app.js.map
|
package/dist/app.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,IAAI,EAA+D,MAAM,MAAM,CAAC;AACnG,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAG3C,OAAO,EAA8C,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAC9F,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,EAAuB,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAEjF,OAAO,EAAsB,iBAAiB,EAAqB,MAAM,YAAY,CAAC;AACtF,OAAO,EAAa,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAa9D,MAAM,iBAAiB,GAAG,kCAAkC,CAAC;AAE7D,MAAM,kBAAkB,GAAG,CAAC,qBAAqB,EAAE,mCAAmC,CAAC,CAAC;AAExF;;;;;GAKG;AACH,MAAM,OAAO,mBAIX,SAAQ,IAAoB;IACnB,eAAe,CAAkB;IACjC,WAAW,CAA+C;IACnE,UAAU,CAAsC;IAEhD,YAAY,IAAkB;QAC5B,KAAK,CAAC,IAAI,CAAC,CAAC;QACZ,IAAI,CAAC,eAAe,GAAG,IAAI,eAAe,EAAE,CAAC;QAC7C,IAAI,CAAC,WAAW,GAAG,IAAI,EAAE,WAAW,CAAC;IACvC,CAAC;IAED,2FAA2F;IAC3F,OAAO,CACL,KAAQ,EACR,OAA2B,EAC3B,IAAwC;QAExC,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,GAAG,KAAK,CAAC;QAClD,IAAI,IAAI,KAAK,IAAI;YAAE,IAAI,CAAC,eAAe,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;QAEjE,MAAM,aAAa,GAAsC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;YACrE,MAAM,QAAQ,GAAG,IAAI,IAAI,IAAI,CAAC,mBAAmB,EAAE,CAAC;YAEpD,OAAO,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAC/B,CAAC,CAAC;QACF,MAAM,QAAQ,GAAG;YACf,GAAG,mBAAmB,CAAC,UAAU,CAAC;YAClC,GAAG,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,OAAO,EAAE,aAAa,CAAC;YACtD,OAAO;SACR,CAAC;QAEF,mGAAmG;QACnG,IAAI,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,GAAG,QAAQ,CAAC,CAAC;QAElE,OAAO,IAAI,CAAC;IACd,CAAC;IAED,gFAAgF;IAChF,KAAK,CACH,IAAa,EACb,GAAyC;QAEzC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAEvB,IAAI,GAAG,YAAY,mBAAmB,EAAE,CAAC;YACvC,GAAG,CAAC,UAAU,KAAK,IAAI,CAAC;YACxB,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;YACpD,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,GAAG,CAAC,eAAe,EAAE,SAAS,CAAC,EAAE,CAAC,SAAS,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;QAC9F,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAED,4DAA4D;IAC5D,kBAAkB,CAAC,MAAsB,EAAE,eAAe,GAAqB,EAAE;QAC/E,OAAO,IAAI,gBAAgB,CAAC,IAAI,CAAC,eAAe,EAAE,eAAe,CAAC,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAC9F,CAAC;IAED,6CAA6C;IAC7C,GAAG,CAAC,IAAY,EAAE,MAAsB,EAAE,eAAe,GAAqB,EAAE;QAC9E,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC;QAE9E,OAAO,IAAI,CAAC;IACd,CAAC;IAED,mGAAmG;IACnG,mBAAmB;QACjB,IAAI,IAAI,CAAC,WAAW,IAAI,IAAI;YAAE,OAAO,IAAI,CAAC,WAAW,CAAC;QAEtD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAyB,CAAC,IAAI,CAAC,CAAC,CAAC;QACxD,IAAI,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC;QAE/B,OAAO,QAAQ,IAAI,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YAClD,IAAI,QAAQ,CAAC,WAAW,IAAI,IAAI;gBAAE,OAAO,QAAQ,CAAC,WAAW,CAAC;YAE9D,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACtB,QAAQ,GAAG,QAAQ,CAAC,UAAU,CAAC;QACjC,CAAC;QAED,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,gBAAgB,CACd,OAAiC,EACjC,IAAuC;QAEvC,IAAI,OAAO,IAAI,IAAI;YAAE,OAAO,EAAE,CAAC;QAE/B,MAAM,UAAU,GAAmC,EAAE,CAAC;QACtD,KAAK,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,eAAe,EAAE,CAAC;YAC9C,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;YAC5B,IAAI,MAAM,IAAI,IAAI;gBAAE,SAAS;YAE7B,UAAU,CAAC,IAAI,CAAC,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,eAAe,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;QACpF,CAAC;QAED,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QAC1B,IAAI,IAAI,IAAI,IAAI;YAAE,UAAU,CAAC,IAAI,CAAC,GAAG,mBAAmB,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,QAAQ,KAAK,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;QAEtG,OAAO,UAAU,CAAC;IACpB,CAAC;CACF;AAED,MAAM,eAAe,GAAG;IACtB,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE;IACjC,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE;IAClC,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE;IACpC,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE;CACsD,CAAC;AAE7F,SAAS,mBAAmB,CAC1B,OAAsB,EACtB,QAAiB,EACjB,IAAuC;IAEvC,MAAM,UAAU,GAAmC,EAAE,CAAC;IAEtD,KAAK,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACzD,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;QAC5B,IAAI,CAAC,oBAAoB,CAAC,MAAM,CAAC;YAAE,SAAS;QAE5C,MAAM,MAAM,GAAG,iBAAiB,CAAC,IAAI,CAAC,SAAS,CAAC;YAC9C,CAAC,CAAC,MAAM;YACR,CAAC,CAAC,kBAAkB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,SAAS,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;gBACnE,CAAC,CAAC,MAAM;gBACR,CAAC,CAAC,SAAS,CAAC;QAChB,IAAI,MAAM,IAAI,IAAI;YAAE,SAAS;QAE7B,MAAM,SAAS,GAAG,iBAAiB,CAAY,MAAM,EAAE,MAAM,EAAE,eAAe,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC;QAC9F,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,kBAAkB,CAAC,SAAS,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3F,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;;;;GAKG;AACH,SAAS,kBAAkB,CACzB,SAAuC,EACvC,SAAiB,EACjB,MAAuB;IAEvB,OAAO,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QACvB,MAAM,WAAW,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;QACjD,IAAI,WAAW,IAAI,IAAI,IAAI,WAAW,CAAC,UAAU,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC;YAChF,OAAO,SAAS,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;QAC5B,CAAC;QAED,CAAC,CAAC,GAAG,CAAC,gBAAgB,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAEnC,MAAM,IAAI,EAAE,CAAC;QAEb,OAAO,SAAS,CAAC;IACnB,CAAC,CAAC;AACJ,CAAC;AAED,4FAA4F;AAC5F,SAAS,eAAe,CAAC,MAAsB;IAC7C,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,aAAa,CAAC,MAAM,EAAE;YACtC,UAAU,EAAE,IAAI,kBAAkB,EAAE;YACpC,SAAS,EAAE,KAAK;YAChB,EAAE,EAAE,OAAO;YACX,MAAM,EAAE,eAAe;SACxB,CAAC,CAAC;QACH,MAAM,UAAU,GAAG,SAAS,CAAC,UAAU,CAAC;QACxC,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,UAAU,IAAI,IAAI;YAAE,OAAO,EAAE,CAAC;QAEpE,OAAO,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACjC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,SAAS,mBAAmB,CAAC,UAAqC;IAChE,IAAI,UAAU,IAAI,IAAI;QAAE,OAAO,EAAE,CAAC;IAClC,IAAI,OAAO,UAAU,KAAK,UAAU;QAAE,OAAO,CAAC,UAAU,CAAC,CAAC;IAE1D,OAAO,CAAC,GAAG,UAAU,CAAC,CAAC;AACzB,CAAC;AAED,SAAS,aAAa,CAAC,IAAY;IACjC,OAAO,IAAI,CAAC,UAAU,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;AAC/C,CAAC;AAWD,yEAAyE;AACzE,sDAAsD;AACtD,MAAM,UAAU,CAAC,CAAgC,GAAM;IACrD,0FAA0F;IAC1F,OAAO,GAAG,CAAC;AACb,CAAC;AAED,OAAO,EAAE,iBAAiB,EAAE,CAAC"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Base class for every error this package throws, so consumers can catch them as a group. */
|
|
2
|
+
export declare class StandardOpenAPIError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
/** Thrown when a schema cannot describe itself as JSON Schema. */
|
|
6
|
+
export declare class UnsupportedSchemaError extends StandardOpenAPIError {
|
|
7
|
+
constructor(vendor?: string);
|
|
8
|
+
}
|
|
9
|
+
/** Thrown when two different schemas claim the same component name. */
|
|
10
|
+
export declare class ComponentNameConflictError extends StandardOpenAPIError {
|
|
11
|
+
constructor(name: string);
|
|
12
|
+
}
|
|
13
|
+
/** Thrown when a schema used for parameters or headers is not a JSON Schema object type. */
|
|
14
|
+
export declare class UnsupportedParameterSchemaError extends StandardOpenAPIError {
|
|
15
|
+
constructor(location: string);
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED,kEAAkE;AAClE,qBAAa,sBAAuB,SAAQ,oBAAoB;IAC9D,YAAY,MAAM,CAAC,EAAE,MAAM,EAK1B;CACF;AAED,uEAAuE;AACvE,qBAAa,0BAA2B,SAAQ,oBAAoB;IAClE,YAAY,IAAI,EAAE,MAAM,EAKvB;CACF;AAED,4FAA4F;AAC5F,qBAAa,+BAAgC,SAAQ,oBAAoB;IACvE,YAAY,QAAQ,EAAE,MAAM,EAK3B;CACF"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Base class for every error this package throws, so consumers can catch them as a group. */
|
|
2
|
+
export class StandardOpenAPIError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = new.target.name;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/** Thrown when a schema cannot describe itself as JSON Schema. */
|
|
9
|
+
export class UnsupportedSchemaError extends StandardOpenAPIError {
|
|
10
|
+
constructor(vendor) {
|
|
11
|
+
super(`Schema${vendor == null ? '' : ` from "${vendor}"`} does not implement Standard JSON Schema. ` +
|
|
12
|
+
'Only schemas exposing `~standard.jsonSchema` can be turned into an OpenAPI document.');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Thrown when two different schemas claim the same component name. */
|
|
16
|
+
export class ComponentNameConflictError extends StandardOpenAPIError {
|
|
17
|
+
constructor(name) {
|
|
18
|
+
super(`Two different schemas are both named "${name}". Component names must be unique; ` +
|
|
19
|
+
'give one of them a different `$id`.');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Thrown when a schema used for parameters or headers is not a JSON Schema object type. */
|
|
23
|
+
export class UnsupportedParameterSchemaError extends StandardOpenAPIError {
|
|
24
|
+
constructor(location) {
|
|
25
|
+
super(`The schema for "${location}" must describe an object, because each of its properties becomes ` +
|
|
26
|
+
'one OpenAPI parameter.');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IAC7C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC;IAC9B,CAAC;CACF;AAED,kEAAkE;AAClE,MAAM,OAAO,sBAAuB,SAAQ,oBAAoB;IAC9D,YAAY,MAAe;QACzB,KAAK,CACH,SAAS,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,MAAM,GAAG,4CAA4C;YAC5F,sFAAsF,CACzF,CAAC;IACJ,CAAC;CACF;AAED,uEAAuE;AACvE,MAAM,OAAO,0BAA2B,SAAQ,oBAAoB;IAClE,YAAY,IAAY;QACtB,KAAK,CACH,yCAAyC,IAAI,qCAAqC;YAChF,qCAAqC,CACxC,CAAC;IACJ,CAAC;CACF;AAED,4FAA4F;AAC5F,MAAM,OAAO,+BAAgC,SAAQ,oBAAoB;IACvE,YAAY,QAAgB;QAC1B,KAAK,CACH,mBAAmB,QAAQ,oEAAoE;YAC7F,wBAAwB,CAC3B,CAAC;IACJ,CAAC;CACF"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { OpenAPIObject } from 'openapi3-ts/oas31';
|
|
2
|
+
import { type NormalizationOptions } from './json-schema.ts';
|
|
3
|
+
import type { OpenAPIDefinition, OpenAPIRegistry } from './registry.ts';
|
|
4
|
+
/** The OpenAPI version to emit. 3.1 is JSON Schema draft 2020-12; 3.0 is its own dialect. */
|
|
5
|
+
export type OpenAPIVersion = '3.0' | '3.1';
|
|
6
|
+
export interface GeneratorOptions {
|
|
7
|
+
readonly version?: OpenAPIVersion;
|
|
8
|
+
readonly normalization?: NormalizationOptions;
|
|
9
|
+
/**
|
|
10
|
+
* How `components.schemas` is ordered.
|
|
11
|
+
*
|
|
12
|
+
* `first-referenced` walks the finished document and orders components the way a reader meets
|
|
13
|
+
* them, which keeps the output stable as unrelated schemas come and go. `registration` keeps the
|
|
14
|
+
* order they were generated in.
|
|
15
|
+
*/
|
|
16
|
+
readonly componentOrder?: 'first-referenced' | 'registration' | 'alphabetical';
|
|
17
|
+
}
|
|
18
|
+
export type DocumentConfig = Omit<OpenAPIObject, 'paths' | 'webhooks'>;
|
|
19
|
+
/** Assembles an OpenAPI document from what a registry collected. */
|
|
20
|
+
export declare class OpenAPIGenerator {
|
|
21
|
+
#private;
|
|
22
|
+
constructor(definitions: OpenAPIDefinition[] | OpenAPIRegistry, options?: GeneratorOptions);
|
|
23
|
+
generateDocument(config: DocumentConfig): OpenAPIObject;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../src/generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,aAAa,EAAmB,MAAM,mBAAmB,CAAC;AAG1F,OAAO,EAA6C,KAAK,oBAAoB,EAAiB,MAAM,kBAAkB,CAAC;AACvH,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAWxE,6FAA6F;AAC7F,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,KAAK,CAAC;AAE3C,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,aAAa,CAAC,EAAE,oBAAoB,CAAC;IAC9C;;;;;;OAMG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,kBAAkB,GAAG,cAAc,GAAG,cAAc,CAAC;CAChF;AAED,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,aAAa,EAAE,OAAO,GAAG,UAAU,CAAC,CAAC;AAevE,oEAAoE;AACpE,qBAAa,gBAAgB;;IAO3B,YAAY,WAAW,EAAE,iBAAiB,EAAE,GAAG,eAAe,EAAE,OAAO,GAAE,gBAAqB,EAO7F;IAED,gBAAgB,CAAC,MAAM,EAAE,cAAc,GAAG,aAAa,CAmCtD;CA6HF"}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { UnsupportedParameterSchemaError, UnsupportedSchemaError } from './errors.js';
|
|
2
|
+
import { ComponentCollector, convertSchema } from './json-schema.js';
|
|
3
|
+
import { isStandardJSONSchema, isStandardSchema } from './standard-schema.js';
|
|
4
|
+
import { PARAMETER_SOURCES, } from './types.js';
|
|
5
|
+
const TARGETS = {
|
|
6
|
+
'3.0': 'openapi-3.0',
|
|
7
|
+
'3.1': 'draft-2020-12',
|
|
8
|
+
};
|
|
9
|
+
/** Assembles an OpenAPI document from what a registry collected. */
|
|
10
|
+
export class OpenAPIGenerator {
|
|
11
|
+
#definitions;
|
|
12
|
+
#options;
|
|
13
|
+
#components = new ComponentCollector();
|
|
14
|
+
/** Names given to schemas from the outside, for schemas that carry no `$id` of their own. */
|
|
15
|
+
#names = new WeakMap();
|
|
16
|
+
constructor(definitions, options = {}) {
|
|
17
|
+
this.#definitions = Array.isArray(definitions) ? definitions : definitions.definitions;
|
|
18
|
+
this.#options = options;
|
|
19
|
+
for (const definition of this.#definitions) {
|
|
20
|
+
if (definition.type === 'schema')
|
|
21
|
+
this.#names.set(definition.schema, definition.name);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
generateDocument(config) {
|
|
25
|
+
const paths = {};
|
|
26
|
+
const webhooks = {};
|
|
27
|
+
const rawComponents = {};
|
|
28
|
+
for (const definition of this.#definitions) {
|
|
29
|
+
switch (definition.type) {
|
|
30
|
+
case 'component':
|
|
31
|
+
rawComponents[definition.componentType] = {
|
|
32
|
+
...rawComponents[definition.componentType],
|
|
33
|
+
[definition.name]: definition.component,
|
|
34
|
+
};
|
|
35
|
+
break;
|
|
36
|
+
case 'schema':
|
|
37
|
+
this.#convert(definition.schema, 'output');
|
|
38
|
+
break;
|
|
39
|
+
case 'route':
|
|
40
|
+
mergePathItem(paths, definition.route, this.#generateOperation(definition.route));
|
|
41
|
+
break;
|
|
42
|
+
case 'webhook':
|
|
43
|
+
mergePathItem(webhooks, definition.webhook, this.#generateOperation(definition.webhook));
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const { components: configuredComponents, ...rest } = config;
|
|
48
|
+
const document = {
|
|
49
|
+
...rest,
|
|
50
|
+
paths,
|
|
51
|
+
components: this.#buildComponents(configuredComponents, rawComponents, paths),
|
|
52
|
+
};
|
|
53
|
+
if (Object.keys(webhooks).length > 0)
|
|
54
|
+
document.webhooks = webhooks;
|
|
55
|
+
return document;
|
|
56
|
+
}
|
|
57
|
+
#buildComponents(configured, raw, paths) {
|
|
58
|
+
const schemas = orderSchemas(this.#components.schemas, this.#options.componentOrder ?? 'registration', paths);
|
|
59
|
+
const merged = { ...configured, ...raw };
|
|
60
|
+
return {
|
|
61
|
+
...merged,
|
|
62
|
+
schemas: { ...merged.schemas, ...schemas },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
#convert(schema, io, hoistRoot) {
|
|
66
|
+
return convertSchema(schema, {
|
|
67
|
+
components: this.#components,
|
|
68
|
+
hoistRoot,
|
|
69
|
+
io,
|
|
70
|
+
name: this.#names.get(schema),
|
|
71
|
+
normalization: this.#options.normalization,
|
|
72
|
+
target: TARGETS[this.#options.version ?? '3.1'],
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
#generateOperation(route) {
|
|
76
|
+
const { method, parameters: declaredParameters, path: _path, request, responses, ...operationConfig } = route;
|
|
77
|
+
const parameters = [...(declaredParameters ?? []), ...this.#generateParameters(request)];
|
|
78
|
+
const operation = { ...operationConfig, responses: this.#generateResponses(responses) };
|
|
79
|
+
if (parameters.length > 0)
|
|
80
|
+
operation.parameters = parameters;
|
|
81
|
+
const requestBody = request?.body;
|
|
82
|
+
if (requestBody != null) {
|
|
83
|
+
const { content, ...bodyConfig } = requestBody;
|
|
84
|
+
operation.requestBody = { ...bodyConfig, content: this.#generateContent(content, 'input') };
|
|
85
|
+
}
|
|
86
|
+
return { method, operation: reorderOperation(operation) };
|
|
87
|
+
}
|
|
88
|
+
#generateParameters(request) {
|
|
89
|
+
if (request == null)
|
|
90
|
+
return [];
|
|
91
|
+
return PARAMETER_SOURCES.flatMap(({ key, location }) => {
|
|
92
|
+
const schema = request[key];
|
|
93
|
+
if (schema == null)
|
|
94
|
+
return [];
|
|
95
|
+
return this.#generateParametersFor(schema, location);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
#generateParametersFor(schema, location) {
|
|
99
|
+
const converted = this.#convert(schema, 'input', false);
|
|
100
|
+
const properties = converted.properties;
|
|
101
|
+
if (converted.type !== 'object' || !isRecord(properties))
|
|
102
|
+
throw new UnsupportedParameterSchemaError(location);
|
|
103
|
+
const required = Array.isArray(converted.required) ? converted.required : [];
|
|
104
|
+
return Object.entries(properties).map(([name, property]) => ({
|
|
105
|
+
...describeParameter(property, location === 'path' || required.includes(name)),
|
|
106
|
+
name,
|
|
107
|
+
in: location,
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
#generateResponses(responses) {
|
|
111
|
+
const generated = {};
|
|
112
|
+
for (const [status, response] of Object.entries(responses)) {
|
|
113
|
+
generated[status] = '$ref' in response ? response : this.#generateResponse(response);
|
|
114
|
+
}
|
|
115
|
+
return generated;
|
|
116
|
+
}
|
|
117
|
+
#generateResponse(response) {
|
|
118
|
+
const { content, description, headers, links } = response;
|
|
119
|
+
const generated = { description };
|
|
120
|
+
if (headers != null)
|
|
121
|
+
generated.headers = this.#generateResponseHeaders(headers);
|
|
122
|
+
if (content != null)
|
|
123
|
+
generated.content = this.#generateContent(content, 'output');
|
|
124
|
+
if (links != null)
|
|
125
|
+
generated.links = links;
|
|
126
|
+
return generated;
|
|
127
|
+
}
|
|
128
|
+
#generateResponseHeaders(headers) {
|
|
129
|
+
if (!isStandardJSONSchema(headers)) {
|
|
130
|
+
if (isStandardSchema(headers))
|
|
131
|
+
throw new UnsupportedSchemaError(headers['~standard'].vendor);
|
|
132
|
+
return headers;
|
|
133
|
+
}
|
|
134
|
+
const converted = this.#convert(headers, 'output', false);
|
|
135
|
+
const properties = converted.properties;
|
|
136
|
+
if (converted.type !== 'object' || !isRecord(properties))
|
|
137
|
+
throw new UnsupportedParameterSchemaError('headers');
|
|
138
|
+
const required = Array.isArray(converted.required) ? converted.required : [];
|
|
139
|
+
const generated = {};
|
|
140
|
+
for (const [name, property] of Object.entries(properties)) {
|
|
141
|
+
generated[name] = describeParameter(property, required.includes(name));
|
|
142
|
+
}
|
|
143
|
+
return generated;
|
|
144
|
+
}
|
|
145
|
+
/** Converts a schema, or passes an already-written OpenAPI schema through untouched. */
|
|
146
|
+
#describe(schema, io) {
|
|
147
|
+
if (isStandardJSONSchema(schema))
|
|
148
|
+
return this.#convert(schema, io);
|
|
149
|
+
if (isStandardSchema(schema))
|
|
150
|
+
throw new UnsupportedSchemaError(schema['~standard'].vendor);
|
|
151
|
+
return schema;
|
|
152
|
+
}
|
|
153
|
+
#generateContent(content, io) {
|
|
154
|
+
const generated = {};
|
|
155
|
+
for (const [mediaType, media] of Object.entries(content)) {
|
|
156
|
+
const { schema, ...rest } = media;
|
|
157
|
+
generated[mediaType] = { ...rest, ...(schema == null ? {} : { schema: this.#describe(schema, io) }) };
|
|
158
|
+
}
|
|
159
|
+
return generated;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Describes one parameter or response header.
|
|
164
|
+
*
|
|
165
|
+
* A description on the schema is repeated on the parameter itself, because tools read it from either
|
|
166
|
+
* place and OpenAPI has no notion of inheriting it.
|
|
167
|
+
*/
|
|
168
|
+
function describeParameter(property, required) {
|
|
169
|
+
const described = { schema: property, required };
|
|
170
|
+
if (isRecord(property) && typeof property.description === 'string')
|
|
171
|
+
described.description = property.description;
|
|
172
|
+
return described;
|
|
173
|
+
}
|
|
174
|
+
/** Puts an operation's keys in the order the OpenAPI specification presents them. */
|
|
175
|
+
function reorderOperation(operation) {
|
|
176
|
+
const { parameters, requestBody, responses, ...rest } = operation;
|
|
177
|
+
const ordered = { ...rest };
|
|
178
|
+
if (parameters != null)
|
|
179
|
+
ordered.parameters = parameters;
|
|
180
|
+
if (requestBody != null)
|
|
181
|
+
ordered.requestBody = requestBody;
|
|
182
|
+
ordered.responses = responses;
|
|
183
|
+
return ordered;
|
|
184
|
+
}
|
|
185
|
+
function mergePathItem(paths, route, { method, operation }) {
|
|
186
|
+
paths[route.path] = { ...paths[route.path], [method]: operation };
|
|
187
|
+
}
|
|
188
|
+
function orderSchemas(schemas, order, paths) {
|
|
189
|
+
if (order === 'registration')
|
|
190
|
+
return schemas;
|
|
191
|
+
if (order === 'alphabetical') {
|
|
192
|
+
return Object.fromEntries(Object.entries(schemas).sort(([left], [right]) => left.localeCompare(right)));
|
|
193
|
+
}
|
|
194
|
+
const ordered = {};
|
|
195
|
+
for (const name of referencedNames(paths, schemas)) {
|
|
196
|
+
const schema = schemas[name];
|
|
197
|
+
if (schema != null)
|
|
198
|
+
ordered[name] = schema;
|
|
199
|
+
}
|
|
200
|
+
return { ...ordered, ...schemas };
|
|
201
|
+
}
|
|
202
|
+
/** Names of components in the order a depth-first read of the document first meets them. */
|
|
203
|
+
function referencedNames(paths, schemas) {
|
|
204
|
+
const seen = [];
|
|
205
|
+
const visit = (node) => {
|
|
206
|
+
if (Array.isArray(node)) {
|
|
207
|
+
for (const entry of node)
|
|
208
|
+
visit(entry);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (!isRecord(node))
|
|
212
|
+
return;
|
|
213
|
+
const reference = node.$ref;
|
|
214
|
+
if (typeof reference === 'string') {
|
|
215
|
+
const name = reference.startsWith('#/components/schemas/') ? reference.slice(21) : undefined;
|
|
216
|
+
if (name == null || seen.includes(name))
|
|
217
|
+
return;
|
|
218
|
+
seen.push(name);
|
|
219
|
+
visit(schemas[name]);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
for (const value of Object.values(node))
|
|
223
|
+
visit(value);
|
|
224
|
+
};
|
|
225
|
+
visit(paths);
|
|
226
|
+
return seen;
|
|
227
|
+
}
|
|
228
|
+
function isRecord(value) {
|
|
229
|
+
return typeof value === 'object' && value != null && !Array.isArray(value);
|
|
230
|
+
}
|
|
231
|
+
//# sourceMappingURL=generator.js.map
|