hono-openapi 0.5.0-rc.2 → 0.5.0-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -31,12 +31,7 @@ const toOpenAPIPath = (path) => path.split("/").map((x) => {
31
31
  return tmp;
32
32
  }).join("/");
33
33
  const capitalize = (word) => word.charAt(0).toUpperCase() + word.slice(1);
34
- const generateOperationIdCache = /* @__PURE__ */ new Map();
35
34
  const generateOperationId = (route) => {
36
- const operationIdKey = `${route.method}:${route.path}`;
37
- if (generateOperationIdCache.has(operationIdKey)) {
38
- return generateOperationIdCache.get(operationIdKey);
39
- }
40
35
  let operationId = route.method;
41
36
  if (route.path === "/") return `${operationId}Index`;
42
37
  for (const segment of route.path.split("/")) {
@@ -46,61 +41,61 @@ const generateOperationId = (route) => {
46
41
  operationId += capitalize(segment);
47
42
  }
48
43
  }
49
- generateOperationIdCache.set(operationIdKey, operationId);
50
44
  return operationId;
51
45
  };
52
46
  const paramKey = (param) => "$ref" in param ? param.$ref : `${param.in} ${param.name}`;
53
47
  function mergeParameters(...params) {
54
- const _params = params.flatMap((x) => x ?? []);
55
- const merged = _params.reduce((acc, param) => {
48
+ const merged = params.flatMap((x) => x ?? []).reduce((acc, param) => {
56
49
  acc.set(paramKey(param), param);
57
50
  return acc;
58
51
  }, /* @__PURE__ */ new Map());
59
52
  return Array.from(merged.values());
60
53
  }
61
- function getProperty(obj, key, defaultValue) {
62
- if (obj != null && key in obj) {
63
- return obj[key];
64
- }
65
- return defaultValue;
66
- }
67
54
  const specsByPathContext = /* @__PURE__ */ new Map();
68
55
  function getPathContext(path) {
69
- const keys = Array.from(specsByPathContext.keys());
70
56
  const context = [];
71
- for (const key of keys) {
72
- if (path.match(key)) {
73
- const data = specsByPathContext.get(key);
74
- if (!data) continue;
57
+ for (const [key, data] of specsByPathContext) {
58
+ if (data && path.match(key)) {
75
59
  context.push(data);
76
60
  }
77
61
  }
78
62
  return context;
79
63
  }
80
- function mergeSpecs(...specs) {
64
+ function mergeSpecs(route, ...specs) {
81
65
  return specs.reduce(
82
66
  (prev, spec) => {
83
- if (!spec) return prev;
84
- return {
85
- ...prev,
86
- ...spec,
87
- tags: Array.from(
88
- /* @__PURE__ */ new Set([
89
- ...getProperty(prev, "tags") ?? [],
90
- ...getProperty(spec, "tags") ?? []
91
- ])
92
- ),
93
- parameters: mergeParameters(
94
- getProperty(prev, "parameters"),
95
- getProperty(spec, "parameters")
96
- ),
97
- responses: {
98
- ...getProperty(prev, "responses", {}),
99
- ...getProperty(spec, "responses", {})
67
+ if (!spec || !prev) return prev;
68
+ for (const [key, value] of Object.entries(spec)) {
69
+ if (value == null) continue;
70
+ if (key in prev && (typeof value === "object" || typeof value === "function" && key === "operationId")) {
71
+ if (Array.isArray(value)) {
72
+ const values = [...prev[key] ?? [], ...value];
73
+ if (key === "tags") {
74
+ prev[key] = Array.from(new Set(values));
75
+ } else {
76
+ prev[key] = values;
77
+ }
78
+ } else if (typeof value === "function") {
79
+ prev[key] = value(route);
80
+ } else {
81
+ if (key === "parameters") {
82
+ prev[key] = mergeParameters(prev[key], value);
83
+ } else {
84
+ prev[key] = {
85
+ ...prev[key],
86
+ ...value
87
+ };
88
+ }
89
+ }
90
+ } else {
91
+ prev[key] = value;
100
92
  }
101
- };
93
+ }
94
+ return prev;
102
95
  },
103
- {}
96
+ {
97
+ operationId: generateOperationId(route)
98
+ }
104
99
  );
105
100
  }
106
101
  function registerSchemaPath({
@@ -114,19 +109,23 @@ function registerSchemaPath({
114
109
  if (!specs) return;
115
110
  if (specsByPathContext.has(path)) {
116
111
  const prev = specsByPathContext.get(path) ?? {};
117
- specsByPathContext.set(path, mergeSpecs(prev, specs));
112
+ specsByPathContext.set(path, mergeSpecs(route, prev, specs));
118
113
  } else {
119
114
  specsByPathContext.set(path, specs);
120
115
  }
121
116
  } else {
122
117
  const pathContext = getPathContext(path);
123
- paths[path] = {
124
- ...paths[path] ? paths[path] : {},
125
- [method]: {
126
- operationId: generateOperationId(route),
127
- ...mergeSpecs(...pathContext, paths[path]?.[method], specs)
128
- }
129
- };
118
+ if (!(path in paths)) {
119
+ paths[path] = {};
120
+ }
121
+ if (paths[path]) {
122
+ paths[path][method] = mergeSpecs(
123
+ route,
124
+ ...pathContext,
125
+ paths[path]?.[method],
126
+ specs
127
+ );
128
+ }
130
129
  }
131
130
  }
132
131
  function removeExcludedPaths(paths, ctx) {
@@ -152,10 +151,21 @@ function removeExcludedPaths(paths, ctx) {
152
151
  for (const param of pathParameters) {
153
152
  const paramName = param.slice(1, param.length - 1);
154
153
  const index = schema.parameters.findIndex(
155
- (x) => x.in === "param" && x.name === paramName
154
+ (x) => {
155
+ if ("$ref" in x) {
156
+ const pos = x.$ref.split("/").pop();
157
+ if (pos) {
158
+ const param2 = ctx.components.parameters?.[pos];
159
+ if (param2 && !("$ref" in param2)) {
160
+ return param2.in === "path" && param2.name === paramName;
161
+ }
162
+ }
163
+ return false;
164
+ }
165
+ return x.in === "path" && x.name === paramName;
166
+ }
156
167
  );
157
- if (index !== -1) schema.parameters[index].in = "path";
158
- else {
168
+ if (index === -1) {
159
169
  schema.parameters.push({
160
170
  schema: { type: "string" },
161
171
  in: "path",
@@ -202,20 +212,24 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
202
212
  }
203
213
  };
204
214
  const _documentation = ctx.options.documentation ?? {};
205
- const schema = await generatePaths(hono, ctx);
206
- for (const path in schema) {
207
- for (const method in schema[path]) {
215
+ const paths = await generatePaths(hono, ctx);
216
+ for (const path in paths) {
217
+ for (const method in paths[path]) {
208
218
  const isHidden = getHiddenValue({
209
- valueOrFunc: schema[path][method]?.hide,
219
+ valueOrFunc: paths[path][method]?.hide,
210
220
  method,
211
221
  path,
212
222
  c
213
223
  });
214
224
  if (isHidden) {
215
- delete schema[path][method];
225
+ paths[path][method] = void 0;
216
226
  }
217
227
  }
218
228
  }
229
+ const components = mergeComponentsObjects(
230
+ _documentation.components,
231
+ ctx.components
232
+ );
219
233
  return {
220
234
  openapi: "3.1.0",
221
235
  ..._documentation,
@@ -229,22 +243,17 @@ async function generateSpecs(hono, options = DEFAULT_OPTIONS, c) {
229
243
  ..._documentation.info
230
244
  },
231
245
  paths: {
232
- ...removeExcludedPaths(schema, ctx),
246
+ ...removeExcludedPaths(paths, ctx),
233
247
  ..._documentation.paths
234
248
  },
235
- components: {
236
- ..._documentation.components,
237
- schemas: {
238
- ...ctx.components.schemas,
239
- ..._documentation.components?.schemas
240
- }
241
- }
249
+ components
242
250
  };
243
251
  }
244
252
  async function generatePaths(hono, ctx) {
245
253
  const paths = {};
246
254
  for (const route of hono.routes) {
247
- if (!(uniqueSymbol in route.handler)) {
255
+ const middlewareHandler = route.handler[uniqueSymbol];
256
+ if (!middlewareHandler) {
248
257
  if (ctx.options.includeEmptyPaths) {
249
258
  registerSchemaPath({
250
259
  route,
@@ -262,20 +271,12 @@ async function generatePaths(hono, ctx) {
262
271
  continue;
263
272
  }
264
273
  }
265
- const middlewareHandler = route.handler[uniqueSymbol];
266
274
  const defaultOptionsForThisMethod = ctx.options.defaultOptions?.[routeMethod];
267
275
  const { schema: routeSpecs, components = {} } = await getSpec(
268
276
  middlewareHandler,
269
277
  defaultOptionsForThisMethod
270
278
  );
271
- ctx.components = {
272
- ...ctx.components,
273
- ...components,
274
- schemas: {
275
- ...ctx.components.schemas,
276
- ...components.schemas
277
- }
278
- };
279
+ ctx.components = mergeComponentsObjects(ctx.components, components);
279
280
  registerSchemaPath({
280
281
  route,
281
282
  specs: routeSpecs,
@@ -289,14 +290,9 @@ function getHiddenValue(options) {
289
290
  if (valueOrFunc != null) {
290
291
  if (typeof valueOrFunc === "boolean") {
291
292
  return valueOrFunc;
292
- } else if (typeof valueOrFunc === "function") {
293
- if (c) {
294
- return valueOrFunc(c);
295
- } else {
296
- console.warn(
297
- `'c' is not defined, cannot evaluate hide function for ${method} ${path}`
298
- );
299
- }
293
+ }
294
+ if (typeof valueOrFunc === "function") {
295
+ return valueOrFunc({ c, method, path });
300
296
  }
301
297
  }
302
298
  return false;
@@ -320,7 +316,7 @@ async function getSpec(middlewareHandler, defaultOptions) {
320
316
  const raw = response.content?.[contentKey];
321
317
  if (!raw) continue;
322
318
  if (raw.schema && "toOpenAPISchema" in raw.schema) {
323
- const result2 = await raw.schema.toOpenAPISchema(defaultOptions);
319
+ const result2 = await raw.schema.toOpenAPISchema();
324
320
  raw.schema = result2.schema;
325
321
  if (result2.components) {
326
322
  components = {
@@ -337,7 +333,7 @@ async function getSpec(middlewareHandler, defaultOptions) {
337
333
  const result = await middlewareHandler.toOpenAPISchema();
338
334
  const docs = {};
339
335
  if (middlewareHandler.target === "form" || middlewareHandler.target === "json") {
340
- const media = middlewareHandler.target === "json" ? "application/json" : "multipart/form-data";
336
+ const media = middlewareHandler.options?.media ?? middlewareHandler.target === "json" ? "application/json" : "multipart/form-data";
341
337
  if (!docs.requestBody || !("content" in docs.requestBody) || !docs.requestBody.content) {
342
338
  docs.requestBody = {
343
339
  content: {
@@ -352,39 +348,78 @@ async function getSpec(middlewareHandler, defaultOptions) {
352
348
  };
353
349
  }
354
350
  } else {
355
- const parameters = [];
351
+ let parameters = [];
356
352
  if ("$ref" in result.schema) {
357
- parameters.push({
358
- in: middlewareHandler.target,
359
- // @ts-expect-error
360
- name: result.schema.$ref,
361
- // @ts-expect-error
362
- schema: result.schema
363
- });
364
- } else {
365
- for (const [key, value] of Object.entries(
366
- result.schema.properties ?? {}
367
- )) {
353
+ const ref = result.schema.$ref;
354
+ const pos = ref.split("/").pop();
355
+ if (pos && result.components?.schemas?.[pos]) {
356
+ const schema = result.components.schemas[pos];
357
+ const newParameters = generateParameters(
358
+ middlewareHandler.target,
359
+ schema
360
+ )[0];
361
+ if (!result.components.parameters) {
362
+ result.components.parameters = {};
363
+ }
364
+ result.components.parameters[pos] = newParameters;
365
+ delete result.components.schemas[pos];
368
366
  parameters.push({
369
- in: middlewareHandler.target,
370
- name: key,
371
- // @ts-expect-error
372
- schema: value,
373
- required: result.schema.required?.includes(key)
367
+ $ref: `#/components/parameters/${pos}`
374
368
  });
375
369
  }
370
+ } else {
371
+ parameters = generateParameters(middlewareHandler.target, result.schema);
376
372
  }
377
373
  docs.parameters = parameters;
378
374
  }
379
375
  return { schema: docs, components: result.components };
380
376
  }
377
+ function generateParameters(target, schema) {
378
+ const parameters = [];
379
+ for (const [key, value] of Object.entries(schema.properties ?? {})) {
380
+ const def = {
381
+ in: target === "param" ? "path" : target,
382
+ name: key,
383
+ // @ts-expect-error
384
+ schema: value,
385
+ required: schema.required?.includes(key)
386
+ };
387
+ if (def.schema && "description" in def.schema && def.schema.description) {
388
+ def.description = def.schema.description;
389
+ def.schema.description = void 0;
390
+ }
391
+ parameters.push(def);
392
+ }
393
+ return parameters;
394
+ }
395
+ function mergeComponentsObjects(...components) {
396
+ return components.reduce(
397
+ (prev, component, index) => {
398
+ if (!component || index === 0) return prev;
399
+ if (prev.schemas && Object.keys(prev.schemas).length > 0 || component.schemas && Object.keys(component.schemas).length > 0) {
400
+ prev.schemas = {
401
+ ...prev.schemas,
402
+ ...component.schemas
403
+ };
404
+ }
405
+ if (prev.parameters && Object.keys(prev.parameters).length > 0 || component.parameters && Object.keys(component.parameters).length > 0) {
406
+ prev.parameters = {
407
+ ...prev.parameters,
408
+ ...component.parameters
409
+ };
410
+ }
411
+ return prev;
412
+ },
413
+ components[0] ?? {}
414
+ );
415
+ }
381
416
 
382
- function resolver(schema) {
417
+ function resolver(schema, options) {
383
418
  return {
384
419
  vendor: schema["~standard"].vendor,
385
420
  validate: schema["~standard"].validate,
386
- toJSONSchema: (options) => standardJson.toJsonSchema(schema, options),
387
- toOpenAPISchema: (options) => standardOpenapi.toOpenAPISchema(schema, options)
421
+ toJSONSchema: () => standardJson.toJsonSchema(schema, options),
422
+ toOpenAPISchema: () => standardOpenapi.toOpenAPISchema(schema, options)
388
423
  };
389
424
  }
390
425
  function validator(target, schema, hook, options) {
@@ -392,7 +427,7 @@ function validator(target, schema, hook, options) {
392
427
  return Object.assign(middleware, {
393
428
  [uniqueSymbol]: {
394
429
  target,
395
- ...resolver(schema),
430
+ ...resolver(schema, options),
396
431
  options
397
432
  }
398
433
  });
@@ -407,16 +442,17 @@ function describeRoute(spec) {
407
442
  }
408
443
  });
409
444
  }
410
- function describeResponse(handler, responses) {
445
+ function describeResponse(handler, responses, options) {
411
446
  const _responses = Object.entries(responses).reduce(
412
447
  (acc, [statusCode, response]) => {
413
448
  if (response.content) {
414
449
  const content = Object.entries(response.content).reduce(
415
450
  (contentAcc, [mediaType, media]) => {
416
451
  if (media.vSchema) {
452
+ const { vSchema, ...rest } = media;
417
453
  contentAcc[mediaType] = {
418
- ...media,
419
- schema: resolver(media.vSchema)
454
+ ...rest,
455
+ schema: resolver(vSchema, options)
420
456
  };
421
457
  } else {
422
458
  contentAcc[mediaType] = media;