peta-docs 0.3.2 → 0.4.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 zfadhli
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.
@@ -0,0 +1,13 @@
1
+ import { p as RouteScanner } from "../types-ByYMmSfu.mjs";
2
+
3
+ //#region src/elysia/scanner.d.ts
4
+ /**
5
+ * Scans an Elysia app instance and extracts route metadata.
6
+ *
7
+ * Relies on Elysia's internal `app.router.history` array. If that structure
8
+ * changes in a future Elysia version, this scanner will warn and
9
+ * return an empty array.
10
+ */
11
+ declare const elysiaScanner: RouteScanner;
12
+ //#endregion
13
+ export { elysiaScanner };
@@ -0,0 +1,35 @@
1
+ import { n as getRouteMeta } from "../route-Dd7Zp1HW.mjs";
2
+ //#region src/elysia/scanner.ts
3
+ /**
4
+ * Scans an Elysia app instance and extracts route metadata.
5
+ *
6
+ * Relies on Elysia's internal `app.router.history` array. If that structure
7
+ * changes in a future Elysia version, this scanner will warn and
8
+ * return an empty array.
9
+ */
10
+ const elysiaScanner = { scan(app) {
11
+ const entries = [];
12
+ if (app == null) return entries;
13
+ const rawRoutes = app.router?.history;
14
+ if (!Array.isArray(rawRoutes)) {
15
+ console.warn("[peta-docs] elysiaScanner: expected app.router.history to be an array, got " + typeof rawRoutes + ". Is this an Elysia app? Provide a custom RouteScanner for other frameworks.");
16
+ return entries;
17
+ }
18
+ for (const r of rawRoutes) {
19
+ const rec = r;
20
+ const handler = rec.handler;
21
+ const meta = typeof handler === "function" ? getRouteMeta(handler) : void 0;
22
+ if (meta) {
23
+ const path = typeof rec.path === "string" ? String(rec.path) : "";
24
+ const method = typeof rec.method === "string" ? String(rec.method) : "";
25
+ entries.push({
26
+ path,
27
+ method,
28
+ config: meta
29
+ });
30
+ }
31
+ }
32
+ return entries;
33
+ } };
34
+ //#endregion
35
+ export { elysiaScanner };
@@ -1,4 +1,4 @@
1
- import { a as FilterFields, f as RouteConfig, g as TypedContext, l as Pagination, n as ArkTypeSchema, o as FilterOperator, r as FieldsetParams, t as RouteScanner } from "../scanner-CU4MsJ2G.mjs";
1
+ import { a as FilterOperator, c as Pagination, d as RouteConfig, g as TypedContext, i as FilterFields, n as FieldsetParams, p as RouteScanner, t as ArkTypeSchema } from "../types-ByYMmSfu.mjs";
2
2
  import { Context, Hono, MiddlewareHandler } from "hono";
3
3
 
4
4
  //#region src/hono/loader.d.ts
@@ -45,13 +45,7 @@ declare function loadRoutes(app: AnyHono, dir: string, options?: {
45
45
  }): Promise<void>;
46
46
  //#endregion
47
47
  //#region src/hono/route.d.ts
48
- interface PaginationOptions {
49
- maxLimit?: number;
50
- defaultLimit?: number;
51
- }
52
48
  type ValidationErrorHandler = (issues: unknown[], c: Context) => Response | Promise<Response>;
53
- /** @deprecated Use {@link RouteBuilder.onValidationError} on the route chain instead. Per-route handlers take precedence over the global handler. */
54
- declare function setOnValidationError(handler: ValidationErrorHandler): () => void;
55
49
  declare class RouteBuilder<B = undefined, Q = undefined, P = undefined, Hd = undefined, Pg extends Pagination | undefined = undefined, F = Record<string, unknown>, Sr = Record<string, unknown>, Ir = Record<string, unknown>, Fs = Record<string, unknown>> {
56
50
  private _config;
57
51
  private static readonly VALIDATOR_MAP;
@@ -77,8 +71,10 @@ declare class RouteBuilder<B = undefined, Q = undefined, P = undefined, Hd = und
77
71
  }, Fs>;
78
72
  fieldsets<R extends string[]>(resources: R): RouteBuilder<B, Q, P, Hd, Pg, F, Sr, Ir, FieldsetParams<R>>;
79
73
  onValidationError(handler: ValidationErrorHandler): this;
80
- onResponseValidationError(handler: ValidationErrorHandler): this;
81
- paginated(options?: PaginationOptions): RouteBuilder<B, Q, P, Hd, Pagination, F, Sr, Ir, Fs>;
74
+ paginated(options?: {
75
+ maxLimit?: number;
76
+ defaultLimit?: number;
77
+ }): RouteBuilder<B, Q, P, Hd, Pagination, F, Sr, Ir, Fs>;
82
78
  handle(handler: (c: TypedContext<B, Q, P, Hd, Pg, F, Sr, Ir, Fs>) => Response | Promise<Response>): MiddlewareHandler;
83
79
  private buildValidators;
84
80
  private buildRouteConfig;
@@ -99,4 +95,4 @@ declare function getRouteMeta(handler: unknown): RouteConfig | undefined;
99
95
  */
100
96
  declare const honoScanner: RouteScanner;
101
97
  //#endregion
102
- export { type PaginationOptions, RouteBuilder, type ValidationErrorHandler, getRouteMeta, honoScanner, loadRoutes, route, setOnValidationError };
98
+ export { RouteBuilder, type ValidationErrorHandler, getRouteMeta, honoScanner, loadRoutes, route };
@@ -1,308 +1,8 @@
1
- import { n as setDefaultScanner } from "../spec-Bobwv7gP.mjs";
2
- import { validator } from "hono/validator";
1
+ import { n as setDefaultScanner } from "../spec-CTxjSu78.mjs";
2
+ import { n as getRouteMeta, r as route, t as RouteBuilder } from "../route-Dd7Zp1HW.mjs";
3
3
  import { readdirSync, statSync } from "node:fs";
4
4
  import { join, resolve } from "node:path";
5
5
  import { Hono } from "hono";
6
- //#region src/hono/route.ts
7
- const OPENAPI_META = Symbol("openapi-meta");
8
- /** Parse a comma-separated string into a trimmed, non-empty array. */
9
- function parseCommaSeparated(value) {
10
- return value ? value.split(",").map((s) => s.trim()).filter(Boolean) : [];
11
- }
12
- /**
13
- * Validate a value against a Standard Schema.
14
- * Returns the validated value, or calls `onError` and returns a Response.
15
- */
16
- async function validateOrError(schema, value, c, onError) {
17
- const result = await schema["~standard"].validate(value);
18
- if (Array.isArray(result)) return onError(result, c);
19
- const r = result;
20
- if (r.issues) return onError([...r.issues], c);
21
- if ("value" in r && r.value !== void 0) return r.value;
22
- return value;
23
- }
24
- let onValidationError = (issues, c) => {
25
- return c.json({
26
- error: "Validation failed",
27
- issues
28
- }, 400);
29
- };
30
- /** @deprecated Use {@link RouteBuilder.onValidationError} on the route chain instead. Per-route handlers take precedence over the global handler. */
31
- function setOnValidationError(handler) {
32
- const prev = onValidationError;
33
- onValidationError = handler;
34
- return () => {
35
- onValidationError = prev;
36
- };
37
- }
38
- const onResponseValidationError = (issues, c) => {
39
- return c.json({
40
- error: "Response validation failed",
41
- issues
42
- }, 500);
43
- };
44
- /** @internal */
45
- function createValidator(target, schema, onError) {
46
- return validator(target, async (value, c) => {
47
- const validated = await validateOrError(schema, value, c, onError);
48
- if (validated instanceof Response) return validated;
49
- return validated;
50
- });
51
- }
52
- /**
53
- * Creates a middleware that checks for the presence of auth credentials.
54
- *
55
- * This is a mechanism-level guard — it verifies that the expected auth
56
- * header/cookie exists, but does NOT verify the credential itself
57
- * (e.g., JWT validity, session data). The app's own middleware or
58
- * handler is responsible for identity verification.
59
- */
60
- function authGuard(schemes) {
61
- return async (c, next) => {
62
- for (const scheme of schemes) {
63
- if (scheme === "bearerAuth") {
64
- if (!c.req.header("Authorization")?.startsWith("Bearer ")) return c.json({ error: "unauthorized" }, 401);
65
- }
66
- if (scheme === "sessionAuth" || scheme === "cookieAuth") {
67
- if (!c.req.header("Cookie")?.includes("session=")) return c.json({ error: "unauthorized" }, 401);
68
- }
69
- }
70
- return await next();
71
- };
72
- }
73
- var RouteBuilder = class RouteBuilder {
74
- _config = { responses: {} };
75
- static VALIDATOR_MAP = [
76
- ["requestBody", "json"],
77
- ["params", "param"],
78
- ["headers", "header"]
79
- ];
80
- summary(s) {
81
- this._config.summary = s;
82
- return this;
83
- }
84
- description(s) {
85
- this._config.description = s;
86
- return this;
87
- }
88
- operationId(s) {
89
- this._config.operationId = s;
90
- return this;
91
- }
92
- tags(...t) {
93
- this._config.tags = t;
94
- return this;
95
- }
96
- deprecated(d = true) {
97
- this._config.deprecated = d;
98
- return this;
99
- }
100
- requestBody(schema) {
101
- this._config.requestBody = schema;
102
- return this;
103
- }
104
- query(schema) {
105
- this._config.query = schema;
106
- return this;
107
- }
108
- params(schema) {
109
- this._config.params = schema;
110
- return this;
111
- }
112
- headers(schema) {
113
- this._config.headers = schema;
114
- return this;
115
- }
116
- response(status, value) {
117
- this._config.responses[String(status)] = value;
118
- return this;
119
- }
120
- auth(scheme = "bearerAuth") {
121
- const acc = this._config.security ?? [];
122
- acc.push(scheme);
123
- this._config.security = acc;
124
- return this;
125
- }
126
- filter(name, schema, options) {
127
- const acc = this._config.filters ?? [];
128
- acc.push({
129
- name,
130
- schema,
131
- operators: options?.operators ?? ["eq"]
132
- });
133
- this._config.filters = acc;
134
- return this;
135
- }
136
- sort(fields) {
137
- this._config.sort = fields;
138
- return this;
139
- }
140
- include(relations) {
141
- this._config.include = relations;
142
- return this;
143
- }
144
- fieldsets(resources) {
145
- this._config.fieldsets = resources;
146
- return this;
147
- }
148
- onValidationError(handler) {
149
- this._config.onValidationError = handler;
150
- return this;
151
- }
152
- onResponseValidationError(handler) {
153
- this._config.onResponseValidationError = handler;
154
- return this;
155
- }
156
- paginated(options) {
157
- this._config.pagination = {
158
- maxLimit: options?.maxLimit ?? 100,
159
- defaultLimit: options?.defaultLimit ?? 20
160
- };
161
- return this;
162
- }
163
- handle(handler) {
164
- const onError = this._config.onValidationError ?? onValidationError;
165
- const validators = this.buildValidators(onError);
166
- if (this._config.security?.length) validators.unshift(authGuard(this._config.security));
167
- const routeConfig = this.buildRouteConfig(handler);
168
- const wrapped = this.composeHandler(validators, handler);
169
- return this.attachRouteMeta(wrapped, routeConfig);
170
- }
171
- buildValidators(onError) {
172
- const validators = [];
173
- const querySchema = this._config.query;
174
- const filters = this._config.filters;
175
- const sortFields = this._config.sort;
176
- const includeFields = this._config.include;
177
- const fieldsetResources = this._config.fieldsets;
178
- const pag = this._config.pagination;
179
- if (querySchema || filters?.length || sortFields || includeFields || fieldsetResources || pag) validators.push(validator("query", async (value, c) => {
180
- let merged = {};
181
- const raw = value;
182
- if (querySchema) {
183
- const validated = await validateOrError(querySchema, value, c, onError);
184
- if (validated instanceof Response) return validated;
185
- merged = {
186
- ...merged,
187
- ...validated
188
- };
189
- }
190
- if (filters) for (const filter of filters) for (const op of filter.operators) {
191
- const paramName = op === "eq" ? filter.name : `${filter.name}__${op}`;
192
- const val = raw[paramName];
193
- if (val === void 0) continue;
194
- if (op === "in") {
195
- const items = parseCommaSeparated(val);
196
- const validatedItems = [];
197
- for (const item of items) {
198
- const validated = await validateOrError(filter.schema, item, c, onError);
199
- if (validated instanceof Response) return validated;
200
- validatedItems.push(validated);
201
- }
202
- merged[paramName] = validatedItems;
203
- } else {
204
- const validated = await validateOrError(filter.schema, val, c, onError);
205
- if (validated instanceof Response) return validated;
206
- merged[paramName] = validated;
207
- }
208
- }
209
- if (sortFields) {
210
- const sortVal = raw.sort;
211
- if (sortVal !== void 0) {
212
- const parts = parseCommaSeparated(sortVal);
213
- const allowed = new Set(sortFields.flatMap((f) => [f, `-${f}`]));
214
- for (const part of parts) if (!allowed.has(part)) return onError([{ message: `Invalid sort field "${part}". Allowed: ${[...allowed].join(", ")}` }], c);
215
- merged.sort = parts;
216
- }
217
- }
218
- const includeFields = this._config.include;
219
- if (includeFields) {
220
- const rawInclude = raw.include;
221
- if (rawInclude !== void 0) {
222
- const parts = parseCommaSeparated(rawInclude);
223
- const allowed = new Set(includeFields);
224
- for (const part of parts) if (!allowed.has(part)) return onError([{ message: `Invalid include "${part}". Allowed: ${[...allowed].join(", ")}` }], c);
225
- merged.include = parts;
226
- }
227
- }
228
- const fieldsetResources = this._config.fieldsets;
229
- if (fieldsetResources) for (const resource of fieldsetResources) {
230
- const paramName = `fields[${resource}]`;
231
- const val = raw[paramName];
232
- if (val !== void 0 && typeof val !== "string") return onError([{ message: `"${paramName}" must be a string` }], c);
233
- if (val !== void 0) merged[paramName] = val;
234
- }
235
- if (pag) {
236
- const page = Math.floor(Number(raw.page ?? "1"));
237
- const limit = Math.min(pag.maxLimit, Math.max(1, Math.floor(Number(raw.limit ?? String(pag.defaultLimit)))));
238
- merged.page = Number.isFinite(page) && page >= 1 ? page : 1;
239
- merged.limit = Number.isFinite(limit) ? limit : pag.defaultLimit;
240
- merged.offset = (merged.page - 1) * merged.limit;
241
- }
242
- return merged;
243
- }));
244
- for (const [key, target] of RouteBuilder.VALIDATOR_MAP) {
245
- const schema = this._config[key];
246
- if (schema != null) validators.push(createValidator(target, schema, onError));
247
- }
248
- return validators;
249
- }
250
- buildRouteConfig(handler) {
251
- return {
252
- ...this._config,
253
- responses: this._config.responses,
254
- pagination: this._config.pagination,
255
- filters: this._config.filters,
256
- sort: this._config.sort,
257
- include: this._config.include,
258
- fieldsets: this._config.fieldsets,
259
- security: this._config.security,
260
- handler
261
- };
262
- }
263
- composeHandler(validators, handler) {
264
- return async (c, _next) => {
265
- const run = async (i) => {
266
- if (i < validators.length) return await validators[i](c, run.bind(null, i + 1)) ?? void 0;
267
- const response = await handler(c) ?? void 0;
268
- if (response) {
269
- const onError = this._config.onResponseValidationError ?? onResponseValidationError;
270
- return await this.validateResponse(response, c, onError) ?? void 0;
271
- }
272
- return response;
273
- };
274
- return run(0);
275
- };
276
- }
277
- async validateResponse(response, c, onError) {
278
- if (!(response.headers.get("content-type") ?? "").includes("application/json")) return response;
279
- if (response.status === 204) return response;
280
- const schema = this._config.responses[String(response.status)];
281
- if (!schema || typeof schema !== "object" && typeof schema !== "function" || !("~standard" in schema)) return response;
282
- try {
283
- const validated = await validateOrError(schema, await response.clone().json(), c, onError);
284
- if (validated instanceof Response) return validated;
285
- return response;
286
- } catch {
287
- return response;
288
- }
289
- }
290
- attachRouteMeta(handler, config) {
291
- Object.defineProperty(handler, OPENAPI_META, {
292
- value: config,
293
- writable: false
294
- });
295
- return handler;
296
- }
297
- };
298
- function route() {
299
- return new RouteBuilder();
300
- }
301
- function getRouteMeta(handler) {
302
- if (typeof handler !== "function") return void 0;
303
- return handler[OPENAPI_META];
304
- }
305
- //#endregion
306
6
  //#region src/hono/scanner.ts
307
7
  function routeProp(obj, key) {
308
8
  if (obj == null) return void 0;
@@ -432,4 +132,4 @@ async function loadRoutes(app, dir, options) {
432
132
  */
433
133
  setDefaultScanner(honoScanner);
434
134
  //#endregion
435
- export { RouteBuilder, getRouteMeta, honoScanner, loadRoutes, route, setOnValidationError };
135
+ export { RouteBuilder, getRouteMeta, honoScanner, loadRoutes, route };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as FilterFields, c as OpenAPIObject, d as ResponseValue, f as RouteConfig, g as TypedContext, h as StatusCode, i as FilterDef, l as Pagination, m as SchemaObject, n as ArkTypeSchema, o as FilterOperator, p as RouteEntry, r as FieldsetParams, s as InfoObject, t as RouteScanner, u as PathItemObject } from "./scanner-CU4MsJ2G.mjs";
1
+ import { a as FilterOperator, c as Pagination, d as RouteConfig, f as RouteEntry, g as TypedContext, h as StatusCode, i as FilterFields, l as PathItemObject, m as SchemaObject, n as FieldsetParams, o as InfoObject, p as RouteScanner, r as FilterDef, s as OpenAPIObject, t as ArkTypeSchema, u as ResponseValue } from "./types-ByYMmSfu.mjs";
2
2
 
3
3
  //#region src/scalar.d.ts
4
4
  interface ScalarUIOptions {
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { i as toOpenAPISchema, r as buildOpenAPISpec, t as getOpenAPISpec } from "./spec-Bobwv7gP.mjs";
1
+ import { i as toOpenAPISchema, r as buildOpenAPISpec, t as getOpenAPISpec } from "./spec-CTxjSu78.mjs";
2
2
  //#region src/scalar.ts
3
3
  function escapeHtml(str) {
4
4
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -0,0 +1,292 @@
1
+ import { validator } from "hono/validator";
2
+ //#region src/hono/route.ts
3
+ const OPENAPI_META = Symbol("openapi-meta");
4
+ /** Parse a comma-separated string into a trimmed, non-empty array. */
5
+ function parseCommaSeparated(value) {
6
+ return value ? value.split(",").map((s) => s.trim()).filter(Boolean) : [];
7
+ }
8
+ /**
9
+ * Validate a value against a Standard Schema.
10
+ * Returns the validated value, or calls `onError` and returns a Response.
11
+ */
12
+ async function validateOrError(schema, value, c, onError) {
13
+ const result = await schema["~standard"].validate(value);
14
+ if (Array.isArray(result)) return onError(result, c);
15
+ const r = result;
16
+ if (r.issues) return onError([...r.issues], c);
17
+ if ("value" in r && r.value !== void 0) return r.value;
18
+ return value;
19
+ }
20
+ const onValidationError = (issues, c) => {
21
+ return c.json({
22
+ error: "Validation failed",
23
+ issues
24
+ }, 400);
25
+ };
26
+ const onResponseValidationError = (issues, c) => {
27
+ return c.json({
28
+ error: "Response validation failed",
29
+ issues
30
+ }, 500);
31
+ };
32
+ /** @internal */
33
+ function createValidator(target, schema, onError) {
34
+ return validator(target, async (value, c) => {
35
+ const validated = await validateOrError(schema, value, c, onError);
36
+ if (validated instanceof Response) return validated;
37
+ return validated;
38
+ });
39
+ }
40
+ /**
41
+ * Creates a middleware that checks for the presence of auth credentials.
42
+ *
43
+ * This is a mechanism-level guard — it verifies that the expected auth
44
+ * header/cookie exists, but does NOT verify the credential itself
45
+ * (e.g., JWT validity, session data). The app's own middleware or
46
+ * handler is responsible for identity verification.
47
+ */
48
+ function authGuard(schemes) {
49
+ return async (c, next) => {
50
+ for (const scheme of schemes) {
51
+ if (scheme === "bearerAuth") {
52
+ if (!c.req.header("Authorization")?.startsWith("Bearer ")) return c.json({ error: "unauthorized" }, 401);
53
+ }
54
+ if (scheme === "sessionAuth" || scheme === "cookieAuth") {
55
+ const cookie = c.req.header("Cookie");
56
+ if (!cookie) return c.json({ error: "unauthorized" }, 401);
57
+ if (!cookie.split(";").some((pair) => pair.trim().split("=")[0] === "session")) return c.json({ error: "unauthorized" }, 401);
58
+ }
59
+ }
60
+ return await next();
61
+ };
62
+ }
63
+ var RouteBuilder = class RouteBuilder {
64
+ _config = { responses: {} };
65
+ static VALIDATOR_MAP = [
66
+ ["requestBody", "json"],
67
+ ["params", "param"],
68
+ ["headers", "header"]
69
+ ];
70
+ summary(s) {
71
+ this._config.summary = s;
72
+ return this;
73
+ }
74
+ description(s) {
75
+ this._config.description = s;
76
+ return this;
77
+ }
78
+ operationId(s) {
79
+ this._config.operationId = s;
80
+ return this;
81
+ }
82
+ tags(...t) {
83
+ this._config.tags = t;
84
+ return this;
85
+ }
86
+ deprecated(d = true) {
87
+ this._config.deprecated = d;
88
+ return this;
89
+ }
90
+ requestBody(schema) {
91
+ this._config.requestBody = schema;
92
+ return this;
93
+ }
94
+ query(schema) {
95
+ this._config.query = schema;
96
+ return this;
97
+ }
98
+ params(schema) {
99
+ this._config.params = schema;
100
+ return this;
101
+ }
102
+ headers(schema) {
103
+ this._config.headers = schema;
104
+ return this;
105
+ }
106
+ response(status, value) {
107
+ this._config.responses[String(status)] = value;
108
+ return this;
109
+ }
110
+ auth(scheme = "bearerAuth") {
111
+ const acc = this._config.security ?? [];
112
+ acc.push(scheme);
113
+ this._config.security = acc;
114
+ return this;
115
+ }
116
+ filter(name, schema, options) {
117
+ const acc = this._config.filters ?? [];
118
+ acc.push({
119
+ name,
120
+ schema,
121
+ operators: options?.operators ?? ["eq"]
122
+ });
123
+ this._config.filters = acc;
124
+ return this;
125
+ }
126
+ sort(fields) {
127
+ this._config.sort = fields;
128
+ return this;
129
+ }
130
+ include(relations) {
131
+ this._config.include = relations;
132
+ return this;
133
+ }
134
+ fieldsets(resources) {
135
+ this._config.fieldsets = resources;
136
+ return this;
137
+ }
138
+ onValidationError(handler) {
139
+ this._config.onValidationError = handler;
140
+ return this;
141
+ }
142
+ paginated(options) {
143
+ this._config.pagination = {
144
+ maxLimit: options?.maxLimit ?? 100,
145
+ defaultLimit: options?.defaultLimit ?? 20
146
+ };
147
+ return this;
148
+ }
149
+ handle(handler) {
150
+ const onError = this._config.onValidationError ?? onValidationError;
151
+ const validators = this.buildValidators(onError);
152
+ if (this._config.security?.length) validators.unshift(authGuard(this._config.security));
153
+ const routeConfig = this.buildRouteConfig(handler);
154
+ const wrapped = this.composeHandler(validators, handler);
155
+ return this.attachRouteMeta(wrapped, routeConfig);
156
+ }
157
+ buildValidators(onError) {
158
+ const validators = [];
159
+ const querySchema = this._config.query;
160
+ const filters = this._config.filters;
161
+ const sortFields = this._config.sort;
162
+ const includeFields = this._config.include;
163
+ const fieldsetResources = this._config.fieldsets;
164
+ const pag = this._config.pagination;
165
+ if (querySchema || filters?.length || sortFields || includeFields || fieldsetResources || pag) validators.push(validator("query", async (value, c) => {
166
+ let merged = {};
167
+ const raw = value;
168
+ if (querySchema) {
169
+ const validated = await validateOrError(querySchema, value, c, onError);
170
+ if (validated instanceof Response) return validated;
171
+ merged = {
172
+ ...merged,
173
+ ...validated
174
+ };
175
+ }
176
+ if (filters) for (const filter of filters) for (const op of filter.operators) {
177
+ const paramName = op === "eq" ? filter.name : `${filter.name}__${op}`;
178
+ const val = raw[paramName];
179
+ if (val === void 0) continue;
180
+ if (op === "in") {
181
+ const items = parseCommaSeparated(val);
182
+ const validatedItems = [];
183
+ for (const item of items) {
184
+ const validated = await validateOrError(filter.schema, item, c, onError);
185
+ if (validated instanceof Response) return validated;
186
+ validatedItems.push(validated);
187
+ }
188
+ merged[paramName] = validatedItems;
189
+ } else {
190
+ const validated = await validateOrError(filter.schema, val, c, onError);
191
+ if (validated instanceof Response) return validated;
192
+ merged[paramName] = validated;
193
+ }
194
+ }
195
+ if (sortFields) {
196
+ const sortVal = raw.sort;
197
+ if (sortVal !== void 0) {
198
+ const parts = parseCommaSeparated(sortVal);
199
+ const allowed = new Set(sortFields.flatMap((f) => [f, `-${f}`]));
200
+ for (const part of parts) if (!allowed.has(part)) return onError([{ message: `Invalid sort field "${part}". Allowed: ${[...allowed].join(", ")}` }], c);
201
+ merged.sort = parts;
202
+ }
203
+ }
204
+ const includeFields = this._config.include;
205
+ if (includeFields) {
206
+ const rawInclude = raw.include;
207
+ if (rawInclude !== void 0) {
208
+ const parts = parseCommaSeparated(rawInclude);
209
+ const allowed = new Set(includeFields);
210
+ for (const part of parts) if (!allowed.has(part)) return onError([{ message: `Invalid include "${part}". Allowed: ${[...allowed].join(", ")}` }], c);
211
+ merged.include = parts;
212
+ }
213
+ }
214
+ const fieldsetResources = this._config.fieldsets;
215
+ if (fieldsetResources) for (const resource of fieldsetResources) {
216
+ const paramName = `fields[${resource}]`;
217
+ const val = raw[paramName];
218
+ if (val !== void 0 && typeof val !== "string") return onError([{ message: `"${paramName}" must be a string` }], c);
219
+ if (val !== void 0) merged[paramName] = val;
220
+ }
221
+ if (pag) {
222
+ const page = Math.floor(Number(raw.page ?? "1"));
223
+ const limit = Math.min(pag.maxLimit, Math.max(1, Math.floor(Number(raw.limit ?? String(pag.defaultLimit)))));
224
+ merged.page = Number.isFinite(page) && page >= 1 ? page : 1;
225
+ merged.limit = Number.isFinite(limit) ? limit : pag.defaultLimit;
226
+ merged.offset = (merged.page - 1) * merged.limit;
227
+ }
228
+ return merged;
229
+ }));
230
+ for (const [key, target] of RouteBuilder.VALIDATOR_MAP) {
231
+ const schema = this._config[key];
232
+ if (schema != null) validators.push(createValidator(target, schema, onError));
233
+ }
234
+ return validators;
235
+ }
236
+ buildRouteConfig(handler) {
237
+ return {
238
+ ...this._config,
239
+ responses: this._config.responses,
240
+ pagination: this._config.pagination,
241
+ filters: this._config.filters,
242
+ sort: this._config.sort,
243
+ include: this._config.include,
244
+ fieldsets: this._config.fieldsets,
245
+ security: this._config.security,
246
+ handler
247
+ };
248
+ }
249
+ composeHandler(validators, handler) {
250
+ return async (c, _next) => {
251
+ const run = async (i) => {
252
+ if (i < validators.length) return await validators[i](c, run.bind(null, i + 1)) ?? void 0;
253
+ const response = await handler(c) ?? void 0;
254
+ if (response) {
255
+ const onError = onResponseValidationError;
256
+ return await this.validateResponse(response, c, onError) ?? void 0;
257
+ }
258
+ return response;
259
+ };
260
+ return run(0);
261
+ };
262
+ }
263
+ async validateResponse(response, c, onError) {
264
+ if (!(response.headers.get("content-type") ?? "").includes("application/json")) return response;
265
+ if (response.status === 204) return response;
266
+ const schema = this._config.responses[String(response.status)];
267
+ if (!schema || typeof schema !== "object" && typeof schema !== "function" || !("~standard" in schema)) return response;
268
+ try {
269
+ const validated = await validateOrError(schema, await response.clone().json(), c, onError);
270
+ if (validated instanceof Response) return validated;
271
+ return response;
272
+ } catch {
273
+ return response;
274
+ }
275
+ }
276
+ attachRouteMeta(handler, config) {
277
+ Object.defineProperty(handler, OPENAPI_META, {
278
+ value: config,
279
+ writable: false
280
+ });
281
+ return handler;
282
+ }
283
+ };
284
+ function route() {
285
+ return new RouteBuilder();
286
+ }
287
+ function getRouteMeta(handler) {
288
+ if (typeof handler !== "function") return void 0;
289
+ return handler[OPENAPI_META];
290
+ }
291
+ //#endregion
292
+ export { getRouteMeta as n, route as r, RouteBuilder as t };
@@ -24,9 +24,7 @@ function toOpenAPISchema(schema) {
24
24
  return {};
25
25
  }
26
26
  function mapContentSchemas(content, convert) {
27
- const result = {};
28
- for (const [mediaType, { schema }] of Object.entries(content)) result[mediaType] = { schema: convert(schema) };
29
- return result;
27
+ return Object.fromEntries(Object.entries(content).map(([mt, v]) => [mt, { schema: convert(v.schema) }]));
30
28
  }
31
29
  const STATUS_DESCRIPTIONS = {
32
30
  "200": "OK",
@@ -73,14 +71,7 @@ function honoPathToOpenAPI(path) {
73
71
  return path.replace(/:(\w+)/g, "{$1}");
74
72
  }
75
73
  function parsePathParams(path) {
76
- const params = [];
77
- const regex = /{(\w+)}/g;
78
- let match = regex.exec(path);
79
- while (match !== null) {
80
- params.push(match[1]);
81
- match = regex.exec(path);
82
- }
83
- return params;
74
+ return [...path.matchAll(/{(\w+)}/g)].map((m) => m[1]);
84
75
  }
85
76
  function extractProperties(schema) {
86
77
  const props = schema.properties;
@@ -93,7 +84,7 @@ function extractProperties(schema) {
93
84
  }));
94
85
  }
95
86
  function autoOperationId(method, path) {
96
- const segments = path.replace(/{(\w+)}/g, (_m, name) => `By${name[0].toUpperCase() + name.slice(1)}`).replace(/:(\w+)/g, (_m, name) => `By${name[0].toUpperCase() + name.slice(1)}`).replace(/\/+/g, "/").replace(/^\//, "").split("/").filter(Boolean);
87
+ const segments = path.replace(/{(\w+)}/g, (_m, name) => `By${name[0].toUpperCase() + name.slice(1)}`).replace(/\/+/g, "/").replace(/^\//, "").split("/").filter(Boolean);
97
88
  return method.toLowerCase() + segments.map((s) => s[0].toUpperCase() + s.slice(1)).join("");
98
89
  }
99
90
  function autoTags(path, basePath) {
@@ -108,10 +99,37 @@ const METHOD_ORDER = {
108
99
  delete: 3,
109
100
  patch: 4
110
101
  };
102
+ const VALID_METHODS = new Set(Object.keys(METHOD_ORDER));
111
103
  function buildOpenAPISpec(routes, info, options) {
112
104
  const basePath = options?.basePath ?? "/api";
113
105
  const s = (schema) => toOpenAPISchema(schema);
114
106
  const paths = {};
107
+ const schemaCount = /* @__PURE__ */ new Map();
108
+ for (const { config } of routes) {
109
+ if (config.requestBody) {
110
+ const normalized = normalizeRequestBody(config.requestBody);
111
+ if (normalized?.content) {
112
+ for (const { schema } of Object.values(normalized.content)) if (typeof schema === "function" && "toJsonSchema" in schema) schemaCount.set(schema, (schemaCount.get(schema) ?? 0) + 1);
113
+ }
114
+ }
115
+ for (const [status, rawResponse] of Object.entries(config.responses)) {
116
+ const normalized = normalizeResponse(status, rawResponse);
117
+ if (normalized?.content) {
118
+ for (const { schema } of Object.values(normalized.content)) if (typeof schema === "function" && "toJsonSchema" in schema) schemaCount.set(schema, (schemaCount.get(schema) ?? 0) + 1);
119
+ }
120
+ }
121
+ }
122
+ const schemaRegistry = /* @__PURE__ */ new Map();
123
+ const componentSchemas = {};
124
+ function refOrInline(schema) {
125
+ if (!(typeof schema === "function" && "toJsonSchema" in schema)) return toOpenAPISchema(schema);
126
+ if ((schemaCount.get(schema) ?? 0) <= 1) return toOpenAPISchema(schema);
127
+ if (schemaRegistry.has(schema)) return { $ref: `#/components/schemas/${schemaRegistry.get(schema)}` };
128
+ const name = `Schema${schemaRegistry.size + 1}`;
129
+ schemaRegistry.set(schema, name);
130
+ componentSchemas[name] = toOpenAPISchema(schema);
131
+ return { $ref: `#/components/schemas/${name}` };
132
+ }
115
133
  const tagged = routes.map((r) => ({
116
134
  ...r,
117
135
  tag: (r.config.tags ?? autoTags(r.path, basePath))[0] ?? "__untagged"
@@ -181,27 +199,37 @@ function buildOpenAPISpec(routes, info, options) {
181
199
  });
182
200
  }
183
201
  if (config.sort) {
184
- const enumValues = config.sort.flatMap((f) => [f, `-${f}`]);
202
+ const itemEnum = config.sort.flatMap((f) => [f, `-${f}`]);
185
203
  parameters.push({
186
204
  name: "sort",
187
205
  in: "query",
188
206
  required: false,
207
+ style: "form",
208
+ explode: false,
189
209
  schema: {
190
- type: "string",
191
- enum: enumValues,
192
- description: "Comma-separated sort fields. Prefix with - for descending."
193
- }
210
+ type: "array",
211
+ items: {
212
+ type: "string",
213
+ enum: itemEnum
214
+ }
215
+ },
216
+ description: "Comma-separated sort fields. Prefix with - for descending."
194
217
  });
195
218
  }
196
219
  if (config.include) parameters.push({
197
220
  name: "include",
198
221
  in: "query",
199
222
  required: false,
223
+ style: "form",
224
+ explode: false,
200
225
  schema: {
201
- type: "string",
202
- enum: config.include,
203
- description: "Comma-separated related resources to sideload."
204
- }
226
+ type: "array",
227
+ items: {
228
+ type: "string",
229
+ enum: config.include
230
+ }
231
+ },
232
+ description: "Comma-separated related resources to sideload."
205
233
  });
206
234
  if (config.fieldsets) for (const resource of config.fieldsets) parameters.push({
207
235
  name: `fields[${resource}]`,
@@ -224,6 +252,7 @@ function buildOpenAPISpec(routes, info, options) {
224
252
  const operation = { operationId: config.operationId ?? autoOperationId(method, path) };
225
253
  if (config.summary) operation.summary = config.summary;
226
254
  if (config.description) operation.description = config.description;
255
+ if (config.deprecated) operation.deprecated = config.deprecated;
227
256
  if (config.tags) operation.tags = config.tags;
228
257
  else operation.tags = autoTags(path, basePath);
229
258
  if (config.security) operation.security = [{ ...Object.fromEntries(config.security.map((s) => [s, []])) }];
@@ -233,48 +262,42 @@ function buildOpenAPISpec(routes, info, options) {
233
262
  if (normalized) operation.requestBody = {
234
263
  description: normalized.description,
235
264
  required: normalized.required,
236
- content: mapContentSchemas(normalized.content, s)
265
+ content: mapContentSchemas(normalized.content, refOrInline)
237
266
  };
238
267
  }
239
268
  const responses = {};
240
269
  for (const [status, rawResponse] of Object.entries(config.responses)) {
241
270
  const normalized = normalizeResponse(status, rawResponse);
242
271
  const resp = { description: normalized.description };
243
- if (normalized.content) resp.content = mapContentSchemas(normalized.content, s);
272
+ if (normalized.content) resp.content = mapContentSchemas(normalized.content, refOrInline);
244
273
  responses[status] = resp;
245
274
  }
246
275
  operation.responses = responses;
247
276
  const methodLower = method.toLowerCase();
248
- const key = [
249
- "get",
250
- "post",
251
- "put",
252
- "delete",
253
- "patch"
254
- ].includes(methodLower) ? methodLower : void 0;
255
- if (key) pathItem[key] = operation;
277
+ if (VALID_METHODS.has(methodLower)) pathItem[methodLower] = operation;
256
278
  }
257
279
  const spec = {
258
280
  openapi: "3.1.0",
259
281
  info,
260
282
  paths
261
283
  };
262
- if (options?.components) spec.components = options.components;
284
+ if (Object.keys(componentSchemas).length > 0) spec.components = {
285
+ ...options?.components ?? {},
286
+ schemas: componentSchemas
287
+ };
288
+ else if (options?.components) spec.components = options.components;
263
289
  return spec;
264
290
  }
265
291
  //#endregion
266
292
  //#region src/spec.ts
267
293
  let _defaultScanner = null;
268
- /**
269
- * Register a default scanner for `getOpenAPISpec`.
270
- * Called automatically by framework adapter modules (e.g., `peta-docs/hono`).
271
- */
294
+ /** @deprecated Import 'peta-docs/hono' which auto-registers the Hono scanner. Pass the scanner as the 3rd argument to getOpenAPISpec instead. */
272
295
  function setDefaultScanner(scanner) {
273
296
  _defaultScanner = scanner;
274
297
  }
275
298
  function getOpenAPISpec(app, info, scanner, options) {
276
299
  const active = scanner ?? _defaultScanner;
277
- if (!active) throw new Error("No RouteScanner provided. Pass one explicitly or import from 'peta-docs/hono' which registers a default scanner.");
300
+ if (!active) throw new Error("No RouteScanner provided. Import 'peta-docs/hono' or pass a scanner explicitly.");
278
301
  return buildOpenAPISpec(active.scan(app), info, options);
279
302
  }
280
303
  //#endregion
@@ -51,6 +51,8 @@ interface ParameterObject {
51
51
  description?: string;
52
52
  required?: boolean;
53
53
  deprecated?: boolean;
54
+ style?: string;
55
+ explode?: boolean;
54
56
  schema: SchemaObject;
55
57
  }
56
58
  interface MediaTypeObject {
@@ -113,6 +115,7 @@ interface RouteConfig {
113
115
  description?: string;
114
116
  operationId?: string;
115
117
  tags?: string[];
118
+ deprecated?: boolean;
116
119
  query?: unknown;
117
120
  params?: unknown;
118
121
  headers?: unknown;
@@ -140,16 +143,8 @@ interface RouteEntry {
140
143
  method: string;
141
144
  config: RouteConfig;
142
145
  }
143
- //#endregion
144
- //#region src/scanner.d.ts
145
- /**
146
- * Scans a framework app instance and extracts registered routes.
147
- *
148
- * Each framework adapter implements this interface to bridge the
149
- * framework-specific route registry to the generic OpenAPI pipeline.
150
- */
151
146
  interface RouteScanner {
152
147
  scan(app: unknown): RouteEntry[];
153
148
  }
154
149
  //#endregion
155
- export { FilterFields as a, OpenAPIObject as c, ResponseValue as d, RouteConfig as f, TypedContext as g, StatusCode as h, FilterDef as i, Pagination as l, SchemaObject as m, ArkTypeSchema as n, FilterOperator as o, RouteEntry as p, FieldsetParams as r, InfoObject as s, RouteScanner as t, PathItemObject as u };
150
+ export { FilterOperator as a, Pagination as c, RouteConfig as d, RouteEntry as f, TypedContext as g, StatusCode as h, FilterFields as i, PathItemObject as l, SchemaObject as m, FieldsetParams as n, InfoObject as o, RouteScanner as p, FilterDef as r, OpenAPIObject as s, ArkTypeSchema as t, ResponseValue as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peta-docs",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "OpenAPI + Scalar docs for Hono (and more), powered by Standard Schema",
@@ -11,7 +11,8 @@
11
11
  },
12
12
  "files": [
13
13
  "dist/",
14
- "README.md"
14
+ "README.md",
15
+ "LICENSE"
15
16
  ],
16
17
  "main": "./dist/index.mjs",
17
18
  "module": "./dist/index.mjs",
@@ -26,14 +27,16 @@
26
27
  "types": "./dist/hono/index.d.mts",
27
28
  "import": "./dist/hono/index.mjs",
28
29
  "default": "./dist/hono/index.mjs"
30
+ },
31
+ "./elysia": {
32
+ "types": "./dist/elysia/index.d.mts",
33
+ "import": "./dist/elysia/index.mjs",
34
+ "default": "./dist/elysia/index.mjs"
29
35
  }
30
36
  },
31
37
  "dependencies": {
32
38
  "hono": "^4"
33
39
  },
34
- "optionalDependencies": {
35
- "arktype": "^2"
36
- },
37
40
  "peerDependencies": {
38
41
  "typescript": "^6.0.3"
39
42
  },
@@ -48,6 +51,8 @@
48
51
  "devDependencies": {
49
52
  "@biomejs/biome": "^2.5.0",
50
53
  "@types/bun": "^1.3.14",
54
+ "elysia": "^1.4.29",
55
+ "arktype": "^2",
51
56
  "tsdown": "^0.22.1"
52
57
  }
53
58
  }