hono-openapi 0.4.5 → 0.4.7

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/README.md CHANGED
@@ -144,18 +144,16 @@ app.get(
144
144
  Now, you can access the OpenAPI specification by visiting `http://localhost:3000/openapi`, and you can use this specification to generate client libraries, documentation, and more. Some tools that I used to generate documentation are -
145
145
 
146
146
  - [Swagger UI](https://github.com/honojs/middleware/tree/main/packages/swagger-ui)
147
- - [Scalar](https://github.com/scalar/scalar/tree/main/packages/hono-api-reference)
147
+ - [Scalar](https://www.npmjs.com/package/@scalar/hono-api-reference)
148
148
 
149
149
  ##### Scalar Example
150
150
 
151
151
  ```ts
152
152
  app.get(
153
153
  "/docs",
154
- apiReference({
154
+ Scalar({
155
155
  theme: "saturn",
156
- spec: {
157
- url: "/openapi",
158
- },
156
+ url: "/openapi",
159
157
  })
160
158
  );
161
159
  ```
@@ -240,6 +238,28 @@ app.get(
240
238
  );
241
239
  ```
242
240
 
241
+ #### Persisting OpenAPI Spec to a file
242
+
243
+ You can save the spec to a file for cache or any other external use.
244
+
245
+ ```ts
246
+ import fs from 'node:fs';
247
+ import { openAPISpecs, generateSpecs } from 'hono-openapi';
248
+
249
+ const options = {/* ... */};
250
+ const app = new Hono()
251
+ .get(
252
+ "/openapi",
253
+ openAPISpecs(app, options),
254
+ );
255
+
256
+ generateSpecs(app, options)
257
+ .then(spec => {
258
+ const pathToSpec = "openapi.json"
259
+ fs.writeFileSync(pathToSpec, JSON.stringify(spec, null, 2));
260
+ })
261
+ ```
262
+
243
263
  ## Contributing
244
264
 
245
265
  We would love to have more contributors involved!
package/arktype.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var e=require("@hono/arktype-validator"),r=require("./toOpenAPISchema.cjs"),a=require("./utils.cjs");function t(e){return{builder:async a=>({schema:await r.convert(e.toJsonSchema())}),validator:r=>{e.assert(r)}}}require("json-schema-walker"),exports.resolver=t,exports.validator=function(r,o,s){const i=e.arktypeValidator(r,o,s);return Object.assign(i,{[a.uniqueSymbol]:{resolver:async e=>a.generateValidatorDocs(r,await t(o).builder(e))}})};
1
+ "use strict";var e=require("@hono/arktype-validator"),r=require("./toOpenAPISchema.cjs"),a=require("./utils.cjs");function t(e){return{builder:async()=>({schema:await r.convert(e.in.toJsonSchema())}),validator:r=>{e.assert(r)}}}require("json-schema-walker"),exports.resolver=t,exports.validator=function(r,o,s){const i=e.arktypeValidator(r,o,s);return Object.assign(i,{[a.uniqueSymbol]:{resolver:async e=>a.generateValidatorDocs(r,await t(o).builder(e))}})};
package/arktype.js CHANGED
@@ -1 +1 @@
1
- import{arktypeValidator as a}from"@hono/arktype-validator";import{c as r}from"./toOpenAPISchema.js";import{u as o,g as t}from"./utils.js";import"json-schema-walker";function s(a){return{builder:async o=>({schema:await r(a.toJsonSchema())}),validator:r=>{a.assert(r)}}}function e(r,e,i){const n=a(r,e,i);return Object.assign(n,{[o]:{resolver:async a=>t(r,await s(e).builder(a))}})}export{s as resolver,e as validator};
1
+ import{arktypeValidator as a}from"@hono/arktype-validator";import{c as r}from"./toOpenAPISchema.js";import{u as o,g as t}from"./utils.js";import"json-schema-walker";function s(a){return{builder:async()=>({schema:await r(a.in.toJsonSchema())}),validator:r=>{a.assert(r)}}}function e(r,e,i){const n=a(r,e,i);return Object.assign(n,{[o]:{resolver:async a=>t(r,await s(e).builder(a))}})}export{s as resolver,e as validator};
package/effect.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var e=require("@hono/effect-validator"),r=require("effect"),a=require("./toOpenAPISchema.cjs"),t=require("./utils.cjs");function i(e){return{builder:async t=>({schema:await a.convert(r.JSONSchema.make(e))}),validator:async a=>{await r.Schema.decodeUnknownPromise(e)(a)}}}require("json-schema-walker"),exports.resolver=i,exports.validator=function(r,a){const o=e.effectValidator(r,a);return Object.assign(o,{[t.uniqueSymbol]:{resolver:async e=>t.generateValidatorDocs(r,await i(a).builder(e))}})};
1
+ "use strict";var e=require("@hono/effect-validator"),r=require("effect"),a=require("./toOpenAPISchema.cjs"),t=require("./utils.cjs");function i(e){return{builder:async()=>({schema:await a.convert(r.JSONSchema.make(e))}),validator:async a=>{await r.Schema.decodeUnknownPromise(e)(a)}}}require("json-schema-walker"),exports.resolver=i,exports.validator=function(r,a){const o=e.effectValidator(r,a);return Object.assign(o,{[t.uniqueSymbol]:{resolver:async e=>t.generateValidatorDocs(r,await i(a).builder(e))}})};
package/effect.js CHANGED
@@ -1 +1 @@
1
- import{effectValidator as o}from"@hono/effect-validator";import{JSONSchema as a,Schema as r}from"effect";import{c as e}from"./toOpenAPISchema.js";import{u as t,g as i}from"./utils.js";import"json-schema-walker";function n(o){return{builder:async r=>({schema:await e(a.make(o))}),validator:async a=>{await r.decodeUnknownPromise(o)(a)}}}function s(a,r){const e=o(a,r);return Object.assign(e,{[t]:{resolver:async o=>i(a,await n(r).builder(o))}})}export{n as resolver,s as validator};
1
+ import{effectValidator as o}from"@hono/effect-validator";import{JSONSchema as a,Schema as r}from"effect";import{c as e}from"./toOpenAPISchema.js";import{u as t,g as i}from"./utils.js";import"json-schema-walker";function n(o){return{builder:async()=>({schema:await e(a.make(o))}),validator:async a=>{await r.decodeUnknownPromise(o)(a)}}}function s(a,r){const e=o(a,r);return Object.assign(e,{[t]:{resolver:async o=>i(a,await n(r).builder(o))}})}export{n as resolver,s as validator};
package/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var e=require("./utils.cjs"),t=require("hono/http-exception");const n=["GET","PUT","POST","DELETE","OPTIONS","HEAD","PATCH","TRACE"],s=e=>e.charAt(0).toUpperCase()+e.slice(1),o=new Map,a=(e,t)=>{const n=`${e}:${t}`;if(o.has(n))return o.get(n);let a=e;if("/"===t)return`${a}Index`;for(const e of t.split("/"))123===e.charCodeAt(0)?a+=`By${s(e.slice(1,-1))}`:a+=s(e);return o.set(n,a),a};function c({path:e,method:t,data:n,schema:s}){e=(e=>e.split("/").map((e=>{let t=e;return t.startsWith(":")&&(t=t.slice(1,t.length),t.endsWith("?")&&(t=t.slice(0,-1)),t=`{${t}}`),t})).join("/"))(e);const o=t.toLowerCase();s[e]={...s[e]?s[e]:{},[o]:{responses:{},operationId:a(o,e),...s[e]?.[o]??{},...n,parameters:r(s[e]?.[o]?.parameters??[],n.parameters??[])}}}function r(e,t){const n=e=>"$ref"in e?e.$ref:`${e.in} ${e.name}`;if(0===e.length||0===t.length)return 0===e.length?t:e;const s=t.reduce(((e,t)=>(e.set(n(t),t),e)),new Map),o=e.reduce(((e,t)=>{const o=n(t),a=s.get(o);return s.delete(o),e.push(a??t),e}),[]);return o.push(...s.values()),o}function i(e,{excludeStaticFile:t=!0,exclude:n=[]}){const s={},o=Array.isArray(n)?n:[n];for(const[n,a]of Object.entries(e))if(!(o.some((e=>"string"==typeof e?n===e:e.test(n)))||n.includes("*")||t&&n.includes("."))){for(const e of Object.keys(a)){const t=a[e];if(n.includes("{")){t.parameters||(t.parameters=[]);const e=n.split("/").filter((e=>e.startsWith("{")&&!t.parameters.find((t=>"path"===t.in&&t.name===e.slice(1,e.length-1)))));for(const n of e){const e=n.slice(1,n.length-1),s=t.parameters.findIndex((t=>"param"===t.in&&t.name===e));-1!==s?t.parameters[s].in="path":t.parameters.push({schema:{type:"string"},in:"path",name:e,required:!0})}}t.responses||(t.responses={200:{}})}s[n]=a}return s}async function p(t,{documentation:s={},excludeStaticFile:o=!0,exclude:a=[],excludeMethods:r=["OPTIONS"],excludeTags:p=[],defaultOptions:l}={documentation:{},excludeStaticFile:!0,exclude:[],excludeMethods:["OPTIONS"],excludeTags:[]},{version:u="3.1.0",components:d={}}={version:"3.1.0",components:{}},m){const h={version:u,components:d},f={};for(const s of t.routes){if(!(e.uniqueSymbol in s.handler))continue;if(r.includes(s.method))continue;if(!1===n.includes(s.method)&&"ALL"!==s.method)continue;const{resolver:t,metadata:o={}}=s.handler[e.uniqueSymbol],a=l?.[s.method],{docs:i,components:p}=await t({...h,...o},a);if(h.components={...h.components,...p??{}},"ALL"===s.method)for(const e of n)c({path:s.path,data:i,method:e,schema:f});else c({method:s.method,path:s.path,data:i,schema:f})}for(const e in f)for(const t in f[e]){const n=f[e][t]?.hide;n&&("boolean"==typeof n?n:m&&n(m))&&delete f[e][t]}return{openapi:h.version,...{...s,tags:s.tags?.filter((e=>!p?.includes(e?.name))),info:{title:"Hono Documentation",description:"Development documentation",version:"0.0.0",...s.info},paths:{...i(f,{excludeStaticFile:o,exclude:a}),...s.paths},components:{...s.components,schemas:{...h.components,...s.components?.schemas}}}}}async function l(e,t,n={}){let s={};const o={...n,...t,responses:{...n?.responses,...t.responses}};if(o.responses)for(const t of Object.keys(o.responses)){const n=o.responses[t];if(n&&"content"in n)for(const t of Object.keys(n.content??{})){const o=n.content?.[t];if(o&&(o.schema&&"builder"in o.schema)){const t=await o.schema.builder(e);o.schema=t.schema,t.components&&(s={...s,...t.components})}}}return{docs:o,components:s}}exports.generateValidatorDocs=e.generateValidatorDocs,exports.uniqueSymbol=e.uniqueSymbol,exports.describeRoute=function(n){const{validateResponse:s,...o}=n;return Object.assign((async(e,o)=>{if(await o(),s&&n.responses){const o=e.res.status,a=e.res.headers.get("content-type");if(o&&a){const c=n.responses[o];if(c&&"content"in c&&c.content){const n=a.split(";")[0],o=c.content[n];if(o?.schema&&"validator"in o.schema)try{let t;const s=e.res.clone();if("application/json"===n?t=await s.json():"text/plain"===n&&(t=await s.text()),!t)throw new Error("No data to validate!");await o.schema.validator(t)}catch(e){let n={status:500,message:"Response validation failed!"};throw"object"==typeof s&&(n={...n,...s}),new t.HTTPException(n.status,{message:n.message,cause:e})}}}}}),{[e.uniqueSymbol]:{resolver:(e,t)=>l(e,o,t)}})},exports.generateRouteSpecs=l,exports.generateSpecs=p,exports.openAPISpecs=function(e,t){const n={version:"3.1.0",components:{}};let s=null;return async o=>(s||(s=await p(e,t,n,o)),o.json(s))};
1
+ "use strict";var e=require("./utils.cjs"),t=require("hono/http-exception");const n=["GET","PUT","POST","DELETE","OPTIONS","HEAD","PATCH","TRACE"],s=e=>e.charAt(0).toUpperCase()+e.slice(1),o=new Map,a=(e,t)=>{const n=`${e}:${t}`;if(o.has(n))return o.get(n);let a=e;if("/"===t)return`${a}Index`;for(const e of t.split("/"))123===e.charCodeAt(0)?a+=`By${s(e.slice(1,-1))}`:a+=s(e);return o.set(n,a),a},r=new Map;function c(e,t,n){return e&&t in e?e[t]??n:n}function i(...e){return e.reduce(((e,t)=>{if(!t)return e;let n;return("tags"in e&&e.tags||"tags"in t&&t.tags)&&(n=Array.from(new Set([...c(e,"tags",[]),...c(t,"tags",[])]))),{...e,...t,tags:n,responses:{...c(e,"responses",{}),...c(t,"responses",{})},parameters:l(e.parameters,t.parameters)}}),{})}function p({path:e,method:t,data:n,schema:s}){e=(e=>e.split("/").map((e=>{let t=e;if(t.startsWith(":")){const e=t.match(/^:([^{?]+)(?:{(.+)})?(\?)?$/);e?t=`{${e[1]}}`:(t=t.slice(1,t.length),t.endsWith("?")&&(t=t.slice(0,-1)),t=`{${t}}`)}return t})).join("/"))(e);const o=t.toLowerCase();if("all"===o){if(!n)return;if(r.has(e)){const t=r.get(e)??{};r.set(e,{...t,...n,parameters:l(t.parameters,n.parameters)})}else r.set(e,n)}else{const t=function(e){const t=Array.from(r.keys());let n={};for(const s of t)e.match(s)&&(n=i(n,r.get(s)??{}));return n}(e);s[e]={...s[e]?s[e]:{},[o]:{responses:{},operationId:a(o,e),...i(t,s[e]?.[o],n)}}}}const u=e=>"$ref"in e?e.$ref:`${e.in} ${e.name}`;function l(...e){const t=e.flatMap((e=>e??[])).reduce(((e,t)=>(e.set(u(t),t),e)),new Map);return Array.from(t.values())}function m(e,{excludeStaticFile:t=!0,exclude:n=[]}){const s={},o=Array.isArray(n)?n:[n];for(const[n,a]of Object.entries(e))if(!o.some((e=>"string"==typeof e?n===e:e.test(n)))&&(!n.includes("*")||n.includes("{"))&&(!t||(!n.includes(".")||n.includes("{")))){for(const e of Object.keys(a)){const t=a[e];if(n.includes("{")){t.parameters||(t.parameters=[]);const e=n.split("/").filter((e=>e.startsWith("{")&&!t.parameters.find((t=>"path"===t.in&&t.name===e.slice(1,e.length-1)))));for(const n of e){const e=n.slice(1,n.length-1),s=t.parameters.findIndex((t=>"param"===t.in&&t.name===e));-1!==s?t.parameters[s].in="path":t.parameters.push({schema:{type:"string"},in:"path",name:e,required:!0})}}t.responses||(t.responses={200:{}})}s[n]=a}return s}const f={documentation:{},excludeStaticFile:!0,exclude:[],excludeMethods:["OPTIONS"],excludeTags:[]},d={version:"3.1.0",components:{}};async function h(t,s=f,o=d,a){const r={...f,...s},c={...d,...o},i=r.documentation??{},u=await async function(t,s,o){const a={};for(const r of t.routes){if(!(e.uniqueSymbol in r.handler)){s.includeEmptyPaths&&p({method:r.method,path:r.path,schema:a});continue}if(s.excludeMethods.includes(r.method))continue;if(!1===n.includes(r.method)&&"ALL"!==r.method)continue;const{resolver:t,metadata:c={}}=r.handler[e.uniqueSymbol],i=s.defaultOptions?.[r.method],{docs:u,components:l}=await t({...o,...c},i);o.components={...o.components,...l??{}},p({method:r.method,path:r.path,data:u,schema:a})}return a}(t,r,c);for(const e in u)for(const t in u[e]){const n=u[e][t]?.hide;if(n){let s=!1;"boolean"==typeof n?s=n:"function"==typeof n&&(a?s=n(a):console.warn(`'c' is not defined, cannot evaluate hide function for ${t} ${e}`)),s&&delete u[e][t]}}return{openapi:c.version,...{...i,tags:i.tags?.filter((e=>!r.excludeTags?.includes(e?.name))),info:{title:"Hono Documentation",description:"Development documentation",version:"0.0.0",...i.info},paths:{...m(u,r),...i.paths},components:{...i.components,schemas:{...c.components,...i.components?.schemas}}}}}async function g(e,t,n={}){let s={};const o={...n,...t,responses:{...n?.responses,...t.responses}};if(o.responses)for(const t of Object.keys(o.responses)){const n=o.responses[t];if(n&&"content"in n)for(const t of Object.keys(n.content??{})){const o=n.content?.[t];if(o&&(o.schema&&"builder"in o.schema)){const t=await o.schema.builder(e);o.schema=t.schema,t.components&&(s={...s,...t.components})}}}return{docs:o,components:s}}exports.generateValidatorDocs=e.generateValidatorDocs,exports.uniqueSymbol=e.uniqueSymbol,exports.describeRoute=function(n){const{validateResponse:s,...o}=n;return Object.assign((async(e,o)=>{if(await o(),s&&n.responses){const o=e.res.status,a=e.res.headers.get("content-type");if(o&&a){const r=n.responses[o];if(r&&"content"in r&&r.content){const n=a.split(";")[0],o=r.content[n];if(o?.schema&&"validator"in o.schema)try{let t;const s=e.res.clone();if("application/json"===n?t=await s.json():"text/plain"===n&&(t=await s.text()),!t)throw new Error("No data to validate!");await o.schema.validator(t)}catch(e){let n={status:500,message:"Response validation failed!"};throw"object"==typeof s&&(n={...n,...s}),new t.HTTPException(n.status,{message:n.message,cause:e})}}}}}),{[e.uniqueSymbol]:{resolver:(e,t)=>g(e,o,t)}})},exports.generateRouteSpecs=g,exports.generateSpecs=h,exports.openAPISpecs=function(e,t){const n={version:"3.1.0",components:{}};let s;return async o=>(s||(s=await h(e,t,n,o)),o.json(s))};
package/index.js CHANGED
@@ -1 +1 @@
1
- import{u as e}from"./utils.js";export{g as generateValidatorDocs}from"./utils.js";import{HTTPException as t}from"hono/http-exception";const n=["GET","PUT","POST","DELETE","OPTIONS","HEAD","PATCH","TRACE"],s=e=>e.charAt(0).toUpperCase()+e.slice(1),o=new Map,a=(e,t)=>{const n=`${e}:${t}`;if(o.has(n))return o.get(n);let a=e;if("/"===t)return`${a}Index`;for(const e of t.split("/"))123===e.charCodeAt(0)?a+=`By${s(e.slice(1,-1))}`:a+=s(e);return o.set(n,a),a};function c({path:e,method:t,data:n,schema:s}){e=(e=>e.split("/").map((e=>{let t=e;return t.startsWith(":")&&(t=t.slice(1,t.length),t.endsWith("?")&&(t=t.slice(0,-1)),t=`{${t}}`),t})).join("/"))(e);const o=t.toLowerCase();s[e]={...s[e]?s[e]:{},[o]:{responses:{},operationId:a(o,e),...s[e]?.[o]??{},...n,parameters:r(s[e]?.[o]?.parameters??[],n.parameters??[])}}}function r(e,t){const n=e=>"$ref"in e?e.$ref:`${e.in} ${e.name}`;if(0===e.length||0===t.length)return 0===e.length?t:e;const s=t.reduce(((e,t)=>(e.set(n(t),t),e)),new Map),o=e.reduce(((e,t)=>{const o=n(t),a=s.get(o);return s.delete(o),e.push(a??t),e}),[]);return o.push(...s.values()),o}function i(e,{excludeStaticFile:t=!0,exclude:n=[]}){const s={},o=Array.isArray(n)?n:[n];for(const[n,a]of Object.entries(e))if(!(o.some((e=>"string"==typeof e?n===e:e.test(n)))||n.includes("*")||t&&n.includes("."))){for(const e of Object.keys(a)){const t=a[e];if(n.includes("{")){t.parameters||(t.parameters=[]);const e=n.split("/").filter((e=>e.startsWith("{")&&!t.parameters.find((t=>"path"===t.in&&t.name===e.slice(1,e.length-1)))));for(const n of e){const e=n.slice(1,n.length-1),s=t.parameters.findIndex((t=>"param"===t.in&&t.name===e));-1!==s?t.parameters[s].in="path":t.parameters.push({schema:{type:"string"},in:"path",name:e,required:!0})}}t.responses||(t.responses={200:{}})}s[n]=a}return s}function p(e,t){const n={version:"3.1.0",components:{}};let s=null;return async o=>(s||(s=await l(e,t,n,o)),o.json(s))}async function l(t,{documentation:s={},excludeStaticFile:o=!0,exclude:a=[],excludeMethods:r=["OPTIONS"],excludeTags:p=[],defaultOptions:l}={documentation:{},excludeStaticFile:!0,exclude:[],excludeMethods:["OPTIONS"],excludeTags:[]},{version:m="3.1.0",components:d={}}={version:"3.1.0",components:{}},u){const h={version:m,components:d},f={};for(const s of t.routes){if(!(e in s.handler))continue;if(r.includes(s.method))continue;if(!1===n.includes(s.method)&&"ALL"!==s.method)continue;const{resolver:t,metadata:o={}}=s.handler[e],a=l?.[s.method],{docs:i,components:p}=await t({...h,...o},a);if(h.components={...h.components,...p??{}},"ALL"===s.method)for(const e of n)c({path:s.path,data:i,method:e,schema:f});else c({method:s.method,path:s.path,data:i,schema:f})}for(const e in f)for(const t in f[e]){const n=f[e][t]?.hide;n&&("boolean"==typeof n?n:u&&n(u))&&delete f[e][t]}return{openapi:h.version,...{...s,tags:s.tags?.filter((e=>!p?.includes(e?.name))),info:{title:"Hono Documentation",description:"Development documentation",version:"0.0.0",...s.info},paths:{...i(f,{excludeStaticFile:o,exclude:a}),...s.paths},components:{...s.components,schemas:{...h.components,...s.components?.schemas}}}}}function m(n){const{validateResponse:s,...o}=n;return Object.assign((async(e,o)=>{if(await o(),s&&n.responses){const o=e.res.status,a=e.res.headers.get("content-type");if(o&&a){const c=n.responses[o];if(c&&"content"in c&&c.content){const n=a.split(";")[0],o=c.content[n];if(o?.schema&&"validator"in o.schema)try{let t;const s=e.res.clone();if("application/json"===n?t=await s.json():"text/plain"===n&&(t=await s.text()),!t)throw new Error("No data to validate!");await o.schema.validator(t)}catch(e){let n={status:500,message:"Response validation failed!"};throw"object"==typeof s&&(n={...n,...s}),new t(n.status,{message:n.message,cause:e})}}}}}),{[e]:{resolver:(e,t)=>d(e,o,t)}})}async function d(e,t,n={}){let s={};const o={...n,...t,responses:{...n?.responses,...t.responses}};if(o.responses)for(const t of Object.keys(o.responses)){const n=o.responses[t];if(n&&"content"in n)for(const t of Object.keys(n.content??{})){const o=n.content?.[t];if(o&&(o.schema&&"builder"in o.schema)){const t=await o.schema.builder(e);o.schema=t.schema,t.components&&(s={...s,...t.components})}}}return{docs:o,components:s}}export{m as describeRoute,d as generateRouteSpecs,l as generateSpecs,p as openAPISpecs,e as uniqueSymbol};
1
+ import{u as e}from"./utils.js";export{g as generateValidatorDocs}from"./utils.js";import{HTTPException as t}from"hono/http-exception";const n=["GET","PUT","POST","DELETE","OPTIONS","HEAD","PATCH","TRACE"],s=e=>e.charAt(0).toUpperCase()+e.slice(1),o=new Map,a=(e,t)=>{const n=`${e}:${t}`;if(o.has(n))return o.get(n);let a=e;if("/"===t)return`${a}Index`;for(const e of t.split("/"))123===e.charCodeAt(0)?a+=`By${s(e.slice(1,-1))}`:a+=s(e);return o.set(n,a),a},r=new Map;function c(e,t,n){return e&&t in e?e[t]??n:n}function i(...e){return e.reduce(((e,t)=>{if(!t)return e;let n;return("tags"in e&&e.tags||"tags"in t&&t.tags)&&(n=Array.from(new Set([...c(e,"tags",[]),...c(t,"tags",[])]))),{...e,...t,tags:n,responses:{...c(e,"responses",{}),...c(t,"responses",{})},parameters:m(e.parameters,t.parameters)}}),{})}function p({path:e,method:t,data:n,schema:s}){e=(e=>e.split("/").map((e=>{let t=e;if(t.startsWith(":")){const e=t.match(/^:([^{?]+)(?:{(.+)})?(\?)?$/);e?t=`{${e[1]}}`:(t=t.slice(1,t.length),t.endsWith("?")&&(t=t.slice(0,-1)),t=`{${t}}`)}return t})).join("/"))(e);const o=t.toLowerCase();if("all"===o){if(!n)return;if(r.has(e)){const t=r.get(e)??{};r.set(e,{...t,...n,parameters:m(t.parameters,n.parameters)})}else r.set(e,n)}else{const t=function(e){const t=Array.from(r.keys());let n={};for(const s of t)e.match(s)&&(n=i(n,r.get(s)??{}));return n}(e);s[e]={...s[e]?s[e]:{},[o]:{responses:{},operationId:a(o,e),...i(t,s[e]?.[o],n)}}}}const f=e=>"$ref"in e?e.$ref:`${e.in} ${e.name}`;function m(...e){const t=e.flatMap((e=>e??[])).reduce(((e,t)=>(e.set(f(t),t),e)),new Map);return Array.from(t.values())}function l(e,{excludeStaticFile:t=!0,exclude:n=[]}){const s={},o=Array.isArray(n)?n:[n];for(const[n,a]of Object.entries(e))if(!o.some((e=>"string"==typeof e?n===e:e.test(n)))&&(!n.includes("*")||n.includes("{"))&&(!t||(!n.includes(".")||n.includes("{")))){for(const e of Object.keys(a)){const t=a[e];if(n.includes("{")){t.parameters||(t.parameters=[]);const e=n.split("/").filter((e=>e.startsWith("{")&&!t.parameters.find((t=>"path"===t.in&&t.name===e.slice(1,e.length-1)))));for(const n of e){const e=n.slice(1,n.length-1),s=t.parameters.findIndex((t=>"param"===t.in&&t.name===e));-1!==s?t.parameters[s].in="path":t.parameters.push({schema:{type:"string"},in:"path",name:e,required:!0})}}t.responses||(t.responses={200:{}})}s[n]=a}return s}const u={documentation:{},excludeStaticFile:!0,exclude:[],excludeMethods:["OPTIONS"],excludeTags:[]},d={version:"3.1.0",components:{}};function h(e,t){const n={version:"3.1.0",components:{}};let s;return async o=>(s||(s=await y(e,t,n,o)),o.json(s))}async function y(t,s=u,o=d,a){const r={...u,...s},c={...d,...o},i=r.documentation??{},f=await async function(t,s,o){const a={};for(const r of t.routes){if(!(e in r.handler)){s.includeEmptyPaths&&p({method:r.method,path:r.path,schema:a});continue}if(s.excludeMethods.includes(r.method))continue;if(!1===n.includes(r.method)&&"ALL"!==r.method)continue;const{resolver:t,metadata:c={}}=r.handler[e],i=s.defaultOptions?.[r.method],{docs:f,components:m}=await t({...o,...c},i);o.components={...o.components,...m??{}},p({method:r.method,path:r.path,data:f,schema:a})}return a}(t,r,c);for(const e in f)for(const t in f[e]){const n=f[e][t]?.hide;if(n){let s=!1;"boolean"==typeof n?s=n:"function"==typeof n&&(a?s=n(a):console.warn(`'c' is not defined, cannot evaluate hide function for ${t} ${e}`)),s&&delete f[e][t]}}return{openapi:c.version,...{...i,tags:i.tags?.filter((e=>!r.excludeTags?.includes(e?.name))),info:{title:"Hono Documentation",description:"Development documentation",version:"0.0.0",...i.info},paths:{...l(f,r),...i.paths},components:{...i.components,schemas:{...c.components,...i.components?.schemas}}}}}function w(n){const{validateResponse:s,...o}=n;return Object.assign((async(e,o)=>{if(await o(),s&&n.responses){const o=e.res.status,a=e.res.headers.get("content-type");if(o&&a){const r=n.responses[o];if(r&&"content"in r&&r.content){const n=a.split(";")[0],o=r.content[n];if(o?.schema&&"validator"in o.schema)try{let t;const s=e.res.clone();if("application/json"===n?t=await s.json():"text/plain"===n&&(t=await s.text()),!t)throw new Error("No data to validate!");await o.schema.validator(t)}catch(e){let n={status:500,message:"Response validation failed!"};throw"object"==typeof s&&(n={...n,...s}),new t(n.status,{message:n.message,cause:e})}}}}}),{[e]:{resolver:(e,t)=>x(e,o,t)}})}async function x(e,t,n={}){let s={};const o={...n,...t,responses:{...n?.responses,...t.responses}};if(o.responses)for(const t of Object.keys(o.responses)){const n=o.responses[t];if(n&&"content"in n)for(const t of Object.keys(n.content??{})){const o=n.content?.[t];if(o&&(o.schema&&"builder"in o.schema)){const t=await o.schema.builder(e);o.schema=t.schema,t.components&&(s={...s,...t.components})}}}return{docs:o,components:s}}export{w as describeRoute,x as generateRouteSpecs,y as generateSpecs,h as openAPISpecs,e as uniqueSymbol};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hono-openapi",
3
3
  "description": "OpenAPI schema generator for Hono",
4
- "version": "0.4.5",
4
+ "version": "0.4.7",
5
5
  "types": "./index.d.ts",
6
6
  "main": "./index.js",
7
7
  "type": "module",
@@ -38,7 +38,7 @@
38
38
  "@hono/zod-validator": "^0.4.1",
39
39
  "@sinclair/typebox": "^0.34.9",
40
40
  "@valibot/to-json-schema": "^1.0.0-beta.3",
41
- "arktype": "^2.0.0-rc.25",
41
+ "arktype": "^2.0.0",
42
42
  "effect": "^3.11.3",
43
43
  "hono": "^4.6.13",
44
44
  "openapi-types": "^12.1.3",
@@ -77,9 +77,6 @@
77
77
  "hono": {
78
78
  "optional": true
79
79
  },
80
- "openapi-types": {
81
- "optional": true
82
- },
83
80
  "valibot": {
84
81
  "optional": true
85
82
  },
package/src/openapi.d.ts CHANGED
@@ -17,7 +17,7 @@ export declare function openAPISpecs<E extends Env = BlankEnv, P extends string
17
17
  * @param Context Route context for hiding routes
18
18
  * @returns OpenAPI specs
19
19
  */
20
- export declare function generateSpecs<E extends Env = BlankEnv, P extends string = string, I extends Input = BlankInput, S extends Schema = BlankSchema>(hono: Hono<E, S, P>, { documentation, excludeStaticFile, exclude, excludeMethods, excludeTags, defaultOptions, }?: OpenApiSpecsOptions, { version, components }?: OpenAPIRouteHandlerConfig, c?: Context<E, P, I>): Promise<{
20
+ export declare function generateSpecs<E extends Env = BlankEnv, P extends string = string, I extends Input = BlankInput, S extends Schema = BlankSchema>(hono: Hono<E, S, P>, options?: OpenApiSpecsOptions, config?: OpenAPIRouteHandlerConfig, c?: Context<E, P, I>): Promise<{
21
21
  tags: OpenAPIV3.TagObject[] | undefined;
22
22
  info: {
23
23
  title: string;
package/src/route.d.ts CHANGED
@@ -29,7 +29,6 @@ export declare function generateRouteSpecs(config: OpenAPIRouteHandlerConfig, do
29
29
  description?: string;
30
30
  externalDocs?: import("openapi-types").OpenAPIV3.ExternalDocumentationObject;
31
31
  operationId?: string;
32
- parameters?: (import("openapi-types").OpenAPIV3.ReferenceObject | import("openapi-types").OpenAPIV3.ParameterObject)[];
33
32
  requestBody?: import("openapi-types").OpenAPIV3.ReferenceObject | import("openapi-types").OpenAPIV3.RequestBodyObject;
34
33
  callbacks?: {
35
34
  [callback: string]: import("openapi-types").OpenAPIV3.ReferenceObject | import("openapi-types").OpenAPIV3.CallbackObject;
@@ -42,6 +41,9 @@ export declare function generateRouteSpecs(config: OpenAPIRouteHandlerConfig, do
42
41
  status: ClientErrorStatusCode | ServerErrorStatusCode;
43
42
  message?: string;
44
43
  };
44
+ parameters?: (import("openapi-types").OpenAPIV3.ParameterObject | (import("openapi-types").OpenAPIV3.ParameterObject & {
45
+ schema: import("./types.js").ResolverResult;
46
+ }))[];
45
47
  };
46
48
  components: {};
47
49
  }>;
package/src/types.d.ts CHANGED
@@ -25,7 +25,7 @@ export type HandlerResponse = {
25
25
  }>;
26
26
  metadata?: Record<string, unknown>;
27
27
  };
28
- export type DescribeRouteOptions = Omit<OpenAPIV3.OperationObject, "responses"> & {
28
+ export type DescribeRouteOptions = Omit<OpenAPIV3.OperationObject, "responses" | "parameters"> & {
29
29
  /**
30
30
  * Pass `true` to hide route from OpenAPI/swagger document
31
31
  */
@@ -49,11 +49,17 @@ export type DescribeRouteOptions = Omit<OpenAPIV3.OperationObject, "responses">
49
49
  };
50
50
  }) | OpenAPIV3.ReferenceObject;
51
51
  };
52
+ /**
53
+ * Parameters of the request
54
+ */
55
+ parameters?: (OpenAPIV3.ParameterObject | (OpenAPIV3.ParameterObject & {
56
+ schema: ResolverResult;
57
+ }))[];
52
58
  };
53
59
  export interface OpenAPIRoute {
54
60
  path: string;
55
- method: (typeof ALLOWED_METHODS)[number];
56
- data: DescribeRouteOptions | Pick<OpenAPIV3.OperationObject, "parameters" | "requestBody">;
61
+ method: (typeof ALLOWED_METHODS)[number] | "ALL";
62
+ data?: DescribeRouteOptions | Pick<OpenAPIV3.OperationObject, "parameters" | "requestBody">;
57
63
  }
58
64
  export type OpenApiSpecsOptions = {
59
65
  /**
@@ -62,6 +68,12 @@ export type OpenApiSpecsOptions = {
62
68
  * @see https://swagger.io/specification/v2/
63
69
  */
64
70
  documentation?: Omit<Partial<OpenAPIV3.Document>, "x-express-openapi-additional-middleware" | "x-express-openapi-validation-strict">;
71
+ /**
72
+ * Include paths which don't have the handlers.
73
+ * This is useful when you want to document the
74
+ * API without implementing it or index all the paths.
75
+ */
76
+ includeEmptyPaths?: boolean;
65
77
  /**
66
78
  * Determine if Swagger should exclude static files.
67
79
  *
package/typebox.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var e=require("@hono/typebox-validator"),r=require("@sinclair/typebox/value"),a=require("./toOpenAPISchema.cjs"),t=require("./utils.cjs");function i(e){return{builder:async r=>({schema:await a.convert(e)}),validator:a=>{r.Value.Parse(e,a)}}}require("json-schema-walker"),exports.resolver=i,exports.validator=function(r,a,o){const s=e.tbValidator(r,a,o);return Object.assign(s,{[t.uniqueSymbol]:{resolver:async e=>t.generateValidatorDocs(r,await i(a).builder(e))}})};
1
+ "use strict";var e=require("@hono/typebox-validator"),r=require("@sinclair/typebox/value"),a=require("./toOpenAPISchema.cjs"),t=require("./utils.cjs");function i(e){return{builder:async()=>({schema:await a.convert(e)}),validator:a=>{r.Value.Parse(e,a)}}}require("json-schema-walker"),exports.resolver=i,exports.validator=function(r,a,o){const s=e.tbValidator(r,a,o);return Object.assign(s,{[t.uniqueSymbol]:{resolver:async e=>t.generateValidatorDocs(r,await i(a).builder(e))}})};
package/typebox.js CHANGED
@@ -1 +1 @@
1
- import{tbValidator as r}from"@hono/typebox-validator";import{Value as o}from"@sinclair/typebox/value";import{c as a}from"./toOpenAPISchema.js";import{u as t,g as e}from"./utils.js";import"json-schema-walker";function i(r){return{builder:async o=>({schema:await a(r)}),validator:a=>{o.Parse(r,a)}}}function s(o,a,s){const n=r(o,a,s);return Object.assign(n,{[t]:{resolver:async r=>e(o,await i(a).builder(r))}})}export{i as resolver,s as validator};
1
+ import{tbValidator as r}from"@hono/typebox-validator";import{Value as o}from"@sinclair/typebox/value";import{c as a}from"./toOpenAPISchema.js";import{u as t,g as e}from"./utils.js";import"json-schema-walker";function i(r){return{builder:async()=>({schema:await a(r)}),validator:a=>{o.Parse(r,a)}}}function s(o,a,s){const n=r(o,a,s);return Object.assign(n,{[t]:{resolver:async r=>e(o,await i(a).builder(r))}})}export{i as resolver,s as validator};
package/utils.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";const e=Symbol("openapi");exports.generateValidatorDocs=async function(e,s){const n=await s,o={};if("form"===e||"json"===e)o.requestBody={content:{["json"===e?"application/json":"multipart/form-data"]:{schema:n.schema}}};else{const s=[];if("$ref"in n.schema)s.push({in:e,name:n.schema.$ref,schema:n.schema});else for(const[o,t]of Object.entries(n.schema.properties??{}))s.push({in:e,name:o,schema:t,required:n.schema.required?.includes(o)});o.parameters=s}return{docs:o,components:n.components}},exports.uniqueSymbol=e;
1
+ "use strict";const e=Symbol("openapi");exports.generateValidatorDocs=async function(e,s){const t=await s,n={};if("form"===e||"json"===e){const s="json"===e?"application/json":"multipart/form-data";n.requestBody&&"content"in n.requestBody&&n.requestBody.content?n.requestBody.content[s]={schema:t.schema}:n.requestBody={content:{[s]:{schema:t.schema}}}}else{const s=[];if("$ref"in t.schema)s.push({in:e,name:t.schema.$ref,schema:t.schema});else for(const[n,o]of Object.entries(t.schema.properties??{}))s.push({in:e,name:n,schema:o,required:t.schema.required?.includes(n)});n.parameters=s}return{docs:n,components:t.components}},exports.uniqueSymbol=e;
package/utils.js CHANGED
@@ -1 +1 @@
1
- const e=Symbol("openapi");async function s(e,s){const n=await s,a={};if("form"===e||"json"===e)a.requestBody={content:{["json"===e?"application/json":"multipart/form-data"]:{schema:n.schema}}};else{const s=[];if("$ref"in n.schema)s.push({in:e,name:n.schema.$ref,schema:n.schema});else for(const[a,o]of Object.entries(n.schema.properties??{}))s.push({in:e,name:a,schema:o,required:n.schema.required?.includes(a)});a.parameters=s}return{docs:a,components:n.components}}export{s as g,e as u};
1
+ const e=Symbol("openapi");async function s(e,s){const n=await s,o={};if("form"===e||"json"===e){const s="json"===e?"application/json":"multipart/form-data";o.requestBody&&"content"in o.requestBody&&o.requestBody.content?o.requestBody.content[s]={schema:n.schema}:o.requestBody={content:{[s]:{schema:n.schema}}}}else{const s=[];if("$ref"in n.schema)s.push({in:e,name:n.schema.$ref,schema:n.schema});else for(const[o,t]of Object.entries(n.schema.properties??{}))s.push({in:e,name:o,schema:t,required:n.schema.required?.includes(o)});o.parameters=s}return{docs:o,components:n.components}}export{s as g,e as u};
package/valibot.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var e=require("@hono/valibot-validator"),r=require("@valibot/to-json-schema"),a=require("valibot"),t=require("./toOpenAPISchema.cjs"),o=require("./utils.cjs");function i(e,o){return{builder:async a=>({schema:await t.convert(r.toJsonSchema(e,o))}),validator:async r=>{await a.parseAsync(e,r)}}}require("json-schema-walker"),exports.resolver=i,exports.validator=function(r,a,t){const s=e.vValidator(r,a,t);return Object.assign(s,{[o.uniqueSymbol]:{resolver:async e=>o.generateValidatorDocs(r,await i(a).builder(e))}})};
1
+ "use strict";var e=require("@hono/valibot-validator"),r=require("@valibot/to-json-schema"),a=require("valibot"),o=require("./toOpenAPISchema.cjs"),t=require("./utils.cjs");function i(e,t){return{builder:async()=>({schema:await o.convert(r.toJsonSchema(e,t))}),validator:async r=>{await a.parseAsync(e,r)}}}require("json-schema-walker"),exports.resolver=i,exports.validator=function(r,a,o){const s=e.vValidator(r,a,o);return Object.assign(s,{[t.uniqueSymbol]:{resolver:async e=>t.generateValidatorDocs(r,await i(a,{errorMode:"ignore"}).builder(e))}})};
package/valibot.js CHANGED
@@ -1 +1 @@
1
- import{vValidator as o}from"@hono/valibot-validator";import{toJsonSchema as a}from"@valibot/to-json-schema";import{parseAsync as t}from"valibot";import{c as r}from"./toOpenAPISchema.js";import{u as i,g as s}from"./utils.js";import"json-schema-walker";function m(o,i){return{builder:async t=>({schema:await r(a(o,i))}),validator:async a=>{await t(o,a)}}}function n(a,t,r){const n=o(a,t,r);return Object.assign(n,{[i]:{resolver:async o=>s(a,await m(t).builder(o))}})}export{m as resolver,n as validator};
1
+ import{vValidator as o}from"@hono/valibot-validator";import{toJsonSchema as r}from"@valibot/to-json-schema";import{parseAsync as a}from"valibot";import{c as t}from"./toOpenAPISchema.js";import{u as i,g as s}from"./utils.js";import"json-schema-walker";function e(o,i){return{builder:async()=>({schema:await t(r(o,i))}),validator:async r=>{await a(o,r)}}}function n(r,a,t){const n=o(r,a,t);return Object.assign(n,{[i]:{resolver:async o=>s(r,await e(a,{errorMode:"ignore"}).builder(o))}})}export{e as resolver,n as validator};