Skip to content

Commit 5be5dad

Browse files
fix: added dynamic operationId and improved the hidden fn
1 parent 2a7083f commit 5be5dad

File tree

9 files changed

+87
-93
lines changed

9 files changed

+87
-93
lines changed

package.json

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66
"main": "dist/index.cjs",
77
"module": "dist/index.js",
88
"types": "dist/index.d.ts",
9-
"files": [
10-
"dist"
11-
],
9+
"files": ["dist"],
1210
"license": "MIT",
1311
"scripts": {
1412
"build": "pkgroll --clean-dist",
@@ -49,7 +47,7 @@
4947
"peerDependencies": {
5048
"@hono/standard-validator": "^0.1.2",
5149
"@sinclair/typebox": "^0.34.9",
52-
"@standard-community/standard-json": "^0.3.0-rc.3",
50+
"@standard-community/standard-json": "^0.3.0-rc.4",
5351
"@standard-community/standard-openapi": "^0.2.0-rc.1",
5452
"@types/json-schema": "^7.0.15",
5553
"arktype": "^2.0.0",
@@ -91,4 +89,4 @@
9189
"vitest": "^3.2.4"
9290
},
9391
"packageManager": "pnpm@10.0.0"
94-
}
92+
}

src/__tests__/__snapshots__/arktype.test.ts.snap

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ exports[`arktype > basic 1`] = `
1616
"get": {
1717
"description": "This is a test route",
1818
"operationId": "GETIndex",
19-
"parameters": [],
2019
"requestBody": {
2120
"content": {
2221
"application/json": {
@@ -93,7 +92,6 @@ exports[`arktype > with metadata 1`] = `
9392
"get": {
9493
"description": "This is a test route",
9594
"operationId": "GETIndex",
96-
"parameters": [],
9795
"requestBody": {
9896
"content": {
9997
"application/json": {

src/__tests__/__snapshots__/effect.test.ts.snap

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ exports[`effect > basic 1`] = `
1616
"get": {
1717
"description": "This is a test route",
1818
"operationId": "GETIndex",
19-
"parameters": [],
2019
"requestBody": {
2120
"content": {
2221
"application/json": {

src/__tests__/__snapshots__/valibot.test.ts.snap

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ exports[`valibot > basic 1`] = `
1616
"get": {
1717
"description": "This is a test route",
1818
"operationId": "GETIndex",
19-
"parameters": [],
2019
"requestBody": {
2120
"content": {
2221
"application/json": {
@@ -95,7 +94,6 @@ exports[`valibot > with metadata 1`] = `
9594
"get": {
9695
"description": "This is a test route",
9796
"operationId": "GETIndex",
98-
"parameters": [],
9997
"requestBody": {
10098
"content": {
10199
"application/json": {

src/__tests__/__snapshots__/zodv3.test.ts.snap

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ exports[`zod v3 > basic 1`] = `
1616
"get": {
1717
"description": "This is a test route",
1818
"operationId": "GETIndex",
19-
"parameters": [],
2019
"requestBody": {
2120
"content": {
2221
"application/json": {

src/__tests__/__snapshots__/zodv4.test.ts.snap

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ exports[`zod v4 > basic 1`] = `
1616
"get": {
1717
"description": "This is a test route",
1818
"operationId": "GETIndex",
19-
"parameters": [],
2019
"requestBody": {
2120
"content": {
2221
"application/json": {
@@ -96,7 +95,6 @@ exports[`zod v4 > with metadata 1`] = `
9695
"get": {
9796
"description": "This is a test route",
9897
"operationId": "GETIndex",
99-
"parameters": [],
10098
"requestBody": {
10199
"content": {
102100
"application/json": {
@@ -167,7 +165,6 @@ exports[`zod v4 > with response description 1`] = `
167165
"get": {
168166
"description": "This is a test route",
169167
"operationId": "GETIndex",
170-
"parameters": [],
171168
"requestBody": {
172169
"content": {
173170
"application/json": {

src/handler.ts

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -85,20 +85,20 @@ export async function generateSpecs<
8585
};
8686

8787
const _documentation = ctx.options.documentation ?? {};
88-
const schema = await generatePaths(hono, ctx);
88+
const paths = await generatePaths(hono, ctx);
8989

9090
// Hide routes
91-
for (const path in schema) {
92-
for (const method in schema[path]) {
91+
for (const path in paths) {
92+
for (const method in paths[path]) {
9393
const isHidden = getHiddenValue({
94-
valueOrFunc: schema[path][method]?.hide,
94+
valueOrFunc: paths[path][method]?.hide,
9595
method,
9696
path,
9797
c,
9898
});
9999

100100
if (isHidden) {
101-
delete schema[path][method];
101+
paths[path][method] = undefined;
102102
}
103103
}
104104
}
@@ -116,7 +116,7 @@ export async function generateSpecs<
116116
..._documentation.info,
117117
},
118118
paths: {
119-
...removeExcludedPaths(schema, ctx),
119+
...removeExcludedPaths(paths, ctx),
120120
..._documentation.paths,
121121
},
122122
components: {
@@ -137,8 +137,12 @@ async function generatePaths<
137137
const paths: OpenAPIV3_1.PathsObject = {};
138138

139139
for (const route of hono.routes) {
140+
const middlewareHandler = route.handler[uniqueSymbol] as
141+
| HandlerUniqueProperty
142+
| undefined;
143+
140144
// Finding routes with uniqueSymbol
141-
if (!(uniqueSymbol in route.handler)) {
145+
if (!middlewareHandler) {
142146
// Include empty paths, if enabled
143147
if (ctx.options.includeEmptyPaths) {
144148
registerSchemaPath({
@@ -165,10 +169,6 @@ async function generatePaths<
165169
}
166170
}
167171

168-
const middlewareHandler = route.handler[
169-
uniqueSymbol
170-
] as HandlerUniqueProperty;
171-
172172
const defaultOptionsForThisMethod =
173173
ctx.options.defaultOptions?.[routeMethod];
174174

@@ -210,13 +210,7 @@ function getHiddenValue(options: {
210210
}
211211

212212
if (typeof valueOrFunc === "function") {
213-
if (c) {
214-
return valueOrFunc(c);
215-
}
216-
217-
console.warn(
218-
`'c' is not defined, cannot evaluate hide function for ${method} ${path}`,
219-
);
213+
return valueOrFunc({ c, method, path });
220214
}
221215
}
222216

src/types.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,13 @@ export type DescribeRouteOptions = Omit<
8585
/**
8686
* Pass `true` to hide route from OpenAPI/swagger document
8787
*/
88-
hide?: boolean | ((c: Context) => boolean);
88+
hide?:
89+
| boolean
90+
| ((props: {
91+
c?: Context;
92+
method: string;
93+
path: string;
94+
}) => boolean);
8995
/**
9096
* Responses of the request
9197
*/
@@ -107,8 +113,8 @@ export type DescribeRouteOptions = Omit<
107113

108114
export type RegisterSchemaPathOptions = {
109115
route: RouterRoute;
110-
specs?:
111-
| DescribeRouteOptions
112-
| Pick<OpenAPIV3_1.OperationObject, "parameters" | "requestBody">;
116+
specs?: DescribeRouteOptions & {
117+
operationId?: string | ((route: RouterRoute) => string);
118+
};
113119
paths: Partial<OpenAPIV3_1.PathsObject>;
114120
};

src/utils.ts

Lines changed: 62 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,7 @@ const toOpenAPIPath = (path: string) =>
5454
const capitalize = (word: string) =>
5555
word.charAt(0).toUpperCase() + word.slice(1);
5656

57-
const generateOperationIdCache = new Map<string, string>();
5857
const generateOperationId = (route: RouterRoute) => {
59-
const operationIdKey = `${route.method}:${route.path}`;
60-
61-
if (generateOperationIdCache.has(operationIdKey)) {
62-
return generateOperationIdCache.get(operationIdKey) as string;
63-
}
64-
6558
let operationId = route.method;
6659

6760
if (route.path === "/") return `${operationId}Index`;
@@ -74,8 +67,6 @@ const generateOperationId = (route: RouterRoute) => {
7467
}
7568
}
7669

77-
generateOperationIdCache.set(operationIdKey, operationId);
78-
7970
return operationId;
8071
};
8172

@@ -85,27 +76,16 @@ const paramKey = (param: Parameter) =>
8576
"$ref" in param ? param.$ref : `${param.in} ${param.name}`;
8677

8778
function mergeParameters(...params: (Parameter[] | undefined)[]): Parameter[] {
88-
const _params = params.flatMap((x) => x ?? []);
89-
90-
const merged = _params.reduce((acc, param) => {
91-
acc.set(paramKey(param), param);
92-
return acc;
93-
}, new Map<string, Parameter>());
79+
const merged = params
80+
.flatMap((x) => x ?? [])
81+
.reduce((acc, param) => {
82+
acc.set(paramKey(param), param);
83+
return acc;
84+
}, new Map<string, Parameter>());
9485

9586
return Array.from(merged.values());
9687
}
9788

98-
function getProperty<T = unknown>(
99-
obj: Record<string, unknown> | undefined,
100-
key: string,
101-
defaultValue?: T,
102-
): T | undefined {
103-
if (obj != null && key in obj) {
104-
return obj[key] as T;
105-
}
106-
return defaultValue;
107-
}
108-
10989
const specsByPathContext = new Map<
11090
string,
11191
RegisterSchemaPathOptions["specs"]
@@ -125,32 +105,52 @@ function getPathContext(path: string) {
125105
}
126106

127107
function mergeSpecs(
128-
...specs: (RegisterSchemaPathOptions["specs"] | undefined)[]
108+
route: RouterRoute,
109+
...specs: RegisterSchemaPathOptions["specs"][]
129110
) {
130-
return specs.reduce(
111+
return specs.reduce<OpenAPIV3_1.OperationObject>(
131112
(prev, spec) => {
132-
if (!spec) return prev;
133-
134-
return {
135-
...prev,
136-
...spec,
137-
tags: Array.from(
138-
new Set([
139-
...(getProperty<string[]>(prev, "tags") ?? []),
140-
...(getProperty<string[]>(spec, "tags") ?? []),
141-
]),
142-
),
143-
parameters: mergeParameters(
144-
getProperty(prev, "parameters"),
145-
getProperty(spec, "parameters"),
146-
),
147-
responses: {
148-
...getProperty(prev, "responses", {}),
149-
...getProperty(spec, "responses", {}),
150-
},
151-
};
113+
if (!spec || !prev) return prev;
114+
115+
for (const [key, value] of Object.entries(spec)) {
116+
if (value == null) continue;
117+
118+
if (
119+
key in prev &&
120+
(typeof value === "object" ||
121+
(typeof value === "function" && key === "operationId"))
122+
) {
123+
if (Array.isArray(value)) {
124+
const values = [...(prev[key] ?? []), ...value];
125+
126+
if (key === "tags") {
127+
prev[key] = Array.from(new Set(values));
128+
} else {
129+
prev[key] = values;
130+
}
131+
} else if (typeof value === "function") {
132+
prev[key] = value(route);
133+
} else {
134+
if (key === "parameters") {
135+
// @ts-expect-error
136+
prev[key] = mergeParameters(prev[key], value);
137+
} else {
138+
prev[key] = {
139+
...prev[key],
140+
...value,
141+
};
142+
}
143+
}
144+
} else {
145+
prev[key] = value;
146+
}
147+
}
148+
149+
return prev;
150+
},
151+
{
152+
operationId: generateOperationId(route),
152153
},
153-
{} as NonNullable<RegisterSchemaPathOptions["specs"]>,
154154
);
155155
}
156156

@@ -171,21 +171,26 @@ export function registerSchemaPath({
171171
if (specsByPathContext.has(path)) {
172172
const prev = specsByPathContext.get(path) ?? {};
173173

174-
specsByPathContext.set(path, mergeSpecs(prev, specs));
174+
specsByPathContext.set(path, mergeSpecs(route, prev, specs));
175175
} else {
176176
// If the specs are not present, we can just set it
177177
specsByPathContext.set(path, specs);
178178
}
179179
} else {
180180
const pathContext = getPathContext(path);
181181

182-
paths[path] = {
183-
...(paths[path] ? paths[path] : {}),
184-
[method]: {
185-
operationId: generateOperationId(route),
186-
...mergeSpecs(...pathContext, paths[path]?.[method], specs),
187-
} satisfies OpenAPIV3_1.OperationObject,
188-
};
182+
if (!(path in paths)) {
183+
paths[path] = {};
184+
}
185+
186+
if (paths[path]) {
187+
paths[path][method] = mergeSpecs(
188+
route,
189+
...pathContext,
190+
paths[path]?.[method],
191+
specs,
192+
);
193+
}
189194
}
190195
}
191196

0 commit comments

Comments
 (0)