Skip to content

Commit

Permalink
feat: swagger ui middleware (#230)
Browse files Browse the repository at this point in the history
* feat(zod-openapi): create swagger-ui jsx component

* feat(zod-openapi): add swagger ui docs

* chore(zod-openapi): add versioning doc

* chore(zod-openapi): fix SwaggerUI component doc

* refactor(zod-openapi): remove jsx comment

* feat(swagger-ui): create new package

* feat(swagger-ui): provides swagger ui middleware and swaggerui component

* feat(zod-openapi): Changed to use @hono/swagger-ui

* chore: add versioning doc

* feat: update ci for @hono/swagger-ui

* refactor: remove package-lock.json

* refactor: reverted the extra changes.

* chore: remove old changeset doc

* refactor(swagger-ui): remove unused file

* refactor(swagger-ui): change input type

* feat(swagger-ui): Select only the options you need.

* chore(swagger-ui): update README

* refactor(swagger-ui): rewrite simple

* refactor(zod-openapi): remove swagger-ui

* chore(swagger-ui): fix readme content

* chore(dep): add @types/swagger-ui-dist for option types support

* feat: implement SwaggerConfig Renderer for mapping config object to html

* feat: extend some option support for swagger-ui-dist

* test: move option rendering test and add some cases

* feat: add manually option for making full customizable

* docs: update swagger-ui middleware README

* fix: do not escape HTML strings

* docs: update README of swagger-ui middleware

* ci: update workflow environment for swagger-ui middleware

---------

Co-authored-by: naporin0624 <[email protected]>
  • Loading branch information
sor4chi and naporin0624 authored Nov 4, 2023
1 parent 296446b commit c608fa9
Show file tree
Hide file tree
Showing 15 changed files with 833 additions and 28 deletions.
5 changes: 5 additions & 0 deletions .changeset/tiny-cats-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hono/swagger-ui': minor
---

Create a package that provides swagger ui in hono
25 changes: 25 additions & 0 deletions .github/workflows/ci-swagger-ui.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: ci-swagger-ui
on:
push:
branches: [main]
paths:
- 'packages/swagger-ui/**'
pull_request:
branches: ['*']
paths:
- 'packages/swagger-ui/**'

jobs:
ci:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./packages/swagger-ui
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.x
- run: yarn install --frozen-lockfile
- run: yarn build
- run: yarn test
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"build:valibot-validator": "yarn workspace @hono/valibot-validator build",
"build:zod-openapi": "yarn build:zod-validator && yarn workspace @hono/zod-openapi build",
"build:typia-validator": "yarn workspace @hono/typia-validator build",
"build:swagger-ui": "yarn workspace @hono/swagger-ui build",
"build": "run-p build:*"
},
"license": "MIT",
Expand Down
Empty file.
143 changes: 143 additions & 0 deletions packages/swagger-ui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# Swagger UI Middleware and Component for Hono

This library, `@hono/swagger-ui`, provides a middleware and a component for integrating Swagger UI with Hono applications. Swagger UI is an interactive documentation interface for APIs compliant with the OpenAPI Specification, making it easier to understand and test API endpoints.

## Installation

```bash
npm install @hono/swagger-ui
# or
yarn add @hono/swagger-ui
```

## Usage

### Middleware Usage

You can use the `swaggerUI` middleware to serve Swagger UI on a specific route in your Hono application. Here's how you can do it:

```ts
import { Hono } from 'hono'
import { swaggerUI } from '@hono/swagger-ui'

const app = new Hono()

// Use the middleware to serve Swagger UI at /ui
app.get('/ui', swaggerUI({ url: '/doc' }))

export default app
```

### Component Usage

If you are using `hono/html`, you can use the `SwaggerUI` component to render Swagger UI within your custom HTML structure. Here's an example:

```ts
import { Hono } from 'hono'
import { html } from 'hono/html'
import { SwaggerUI } from '@hono/swagger-ui'

const app = new Hono()

app.get('/ui', (c) => {
return c.html(html`
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Custom Swagger" />
<title>Custom Swagger</title>
<script>
// custom script
</script>
<style>
/* custom style */
</style>
</head>
${SwaggerUI({ url: '/doc' })}
</html>
`)
export default app
```
In this example, the `SwaggerUI` component is used to render Swagger UI within a custom HTML structure, allowing for additional customization such as adding custom scripts and styles.
### With `OpenAPIHono` Usage
Hono's middleware has OpenAPI integration `@hono/zod-openapi`, so you can use it to create an OpenAPI document and serve it easily with Swagger UI.
```ts
import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi'
import { swaggerUI } from '@hono/swagger-ui'

const app = new OpenAPIHono()

app.openapi(
createRoute({
method: 'get',
path: '/hello',
responses: {
200: {
description: 'Respond a message',
content: {
'application/json': {
schema: z.object({
message: z.string()
})
}
}
}
}
}),
(c) => {
return c.jsonT({
message: 'hello'
})
}
)

app.get(
'/ui',
swaggerUI({
url: '/doc'
})
)

app.doc('/doc', {
info: {
title: 'An API',
version: 'v1'
},
openapi: '3.1.0'
})

export default app
```
## Options
Both the middleware and the component accept an options object for customization.
The following options are available:
- `version` (string, optional): The version of Swagger UI to use, defaults to `latest`.
- `manuallySwaggerUIHtml` (string, optional): If you want to use your own custom HTML, you can specify it here. If this option is specified, the all options except `version` will be ignored.
and most of options from [Swagger UI](
https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/
) are supported as well.
such as:
- `url` (string, optional): The URL pointing to the OpenAPI definition (v2 or v3) that describes the API.
- `urls` (array, optional): An array of OpenAPI definitions (v2 or v3) that describe the APIs. Each definition must have a `name` and `url`.
- `presets` (array, optional): An array of presets to use for Swagger UI.
- `plugins` (array, optional): An array of plugins to use for Swagger UI.
## Authors
- naporitan <https://github.com/naporin0624>
- sor4chi <https://github.com/sor4chi>
## License
MIT
52 changes: 52 additions & 0 deletions packages/swagger-ui/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"name": "@hono/swagger-ui",
"version": "0.0.0",
"description": "A middleware for using SwaggerUI in Hono",
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "dist/index.d.cts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
},
"files": [
"dist"
],
"scripts": {
"test": "vitest run",
"build": "tsup ./src/index.ts --format esm,cjs --dts",
"publint": "publint",
"release": "yarn build && yarn test && yarn publint && yarn publish"
},
"license": "MIT",
"private": false,
"publishConfig": {
"registry": "https://registry.npmjs.org",
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/honojs/middleware.git"
},
"homepage": "https://github.com/honojs/middleware",
"peerDependencies": {
"hono": "*"
},
"devDependencies": {
"@types/swagger-ui-dist": "^3.30.3",
"hono": "^3.7.2",
"publint": "^0.2.2",
"tsup": "^7.2.0",
"vite": "^4.4.9",
"vitest": "^0.34.5"
}
}
87 changes: 87 additions & 0 deletions packages/swagger-ui/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import type { Env, MiddlewareHandler } from 'hono'
import { html } from 'hono/html'
import type { DistSwaggerUIOptions } from './swagger/renderer'
import { renderSwaggerUIOptions } from './swagger/renderer'
import type { AssetURLs } from './swagger/resource'
import { remoteAssets } from './swagger/resource'

type OriginalSwaggerUIOptions = {
version?: string
/**
* manuallySwaggerUIHtml is a string that is used to render SwaggerUI.
* If this is set, all other options will be ignored except version.
* The string will be inserted into the body of the HTML.
* This is useful when you want to fully customize the UI.
*
* @example
* ```ts
* const swaggerUI = SwaggerUI({
* manuallySwaggerUIHtml: (asset) => `
* <div>
* <div id="swagger-ui"></div>
* ${asset.css.map((url) => `<link rel="stylesheet" href="${url}" />`)}
* ${asset.js.map((url) => `<script src="${url}" crossorigin="anonymous"></script>`)}
* <script>
* window.onload = () => {
* window.ui = SwaggerUIBundle({
* dom_id: '#swagger-ui',
* url: 'https://petstore.swagger.io/v2/swagger.json',
* })
* }
* </script>
* </div>
* `,
* })
* ```
*/
manuallySwaggerUIHtml?: (asset: AssetURLs) => string
}

type SwaggerUIOptions = OriginalSwaggerUIOptions & DistSwaggerUIOptions

const SwaggerUI = (options: SwaggerUIOptions) => {
const asset = remoteAssets({ version: options?.version })
delete options.version

if (options.manuallySwaggerUIHtml) {
return options.manuallySwaggerUIHtml(asset)
}

const optionsStrings = renderSwaggerUIOptions(options)

return `
<div>
<div id="swagger-ui"></div>
${asset.css.map((url) => html`<link rel="stylesheet" href="${url}" />`)}
${asset.js.map((url) => html`<script src="${url}" crossorigin="anonymous"></script>`)}
<script>
window.onload = () => {
window.ui = SwaggerUIBundle({
dom_id: '#swagger-ui',${optionsStrings},
})
}
</script>
</div>
`
}

const middleware =
<E extends Env>(options: SwaggerUIOptions): MiddlewareHandler<E> =>
async (c) => {
return c.html(/* html */ `
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="SwaggerUI" />
<title>SwaggerUI</title>
</head>
<body>
${SwaggerUI(options)}
</body>
</html>
`)
}

export { middleware as swaggerUI, SwaggerUI }
export { SwaggerUIOptions }
67 changes: 67 additions & 0 deletions packages/swagger-ui/src/swagger/renderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type { SwaggerConfigs } from 'swagger-ui-dist'

export type DistSwaggerUIOptions = {
configUrl?: SwaggerConfigs['configUrl']
deepLinking?: SwaggerConfigs['deepLinking']
presets?: string[]
plugins?: string[]
spec?: SwaggerConfigs['spec']
url?: SwaggerConfigs['url']
urls?: SwaggerConfigs['urls']
layout?: SwaggerConfigs['layout']
docExpansion?: SwaggerConfigs['docExpansion']
maxDisplayedTags?: SwaggerConfigs['maxDisplayedTags']
operationsSorter?: string
requestInterceptor?: string
responseInterceptor?: string
}

const RENDER_TYPE = {
STRING_ARRAY: 'string_array',
STRING: 'string',
JSON_STRING: 'json_string',
RAW: 'raw',
} as const

const RENDER_TYPE_MAP = {
configUrl: RENDER_TYPE.STRING,
deepLinking: RENDER_TYPE.RAW,
presets: RENDER_TYPE.STRING_ARRAY,
plugins: RENDER_TYPE.STRING_ARRAY,
spec: RENDER_TYPE.JSON_STRING,
url: RENDER_TYPE.STRING,
urls: RENDER_TYPE.JSON_STRING,
layout: RENDER_TYPE.STRING,
docExpansion: RENDER_TYPE.STRING,
maxDisplayedTags: RENDER_TYPE.RAW,
operationsSorter: RENDER_TYPE.RAW,
requestInterceptor: RENDER_TYPE.RAW,
responseInterceptor: RENDER_TYPE.RAW,
} as const satisfies Record<
keyof DistSwaggerUIOptions,
(typeof RENDER_TYPE)[keyof typeof RENDER_TYPE]
>

export const renderSwaggerUIOptions = (options: DistSwaggerUIOptions) => {
const optionsStrings = Object.entries(options)
.map(([k, v]) => {
const key = k as keyof typeof RENDER_TYPE_MAP
if (RENDER_TYPE_MAP[key] === RENDER_TYPE.STRING) {
return `${key}: '${v}'`
}
if (RENDER_TYPE_MAP[key] === RENDER_TYPE.STRING_ARRAY) {
if (!Array.isArray(v)) return ''
return `${key}: [${v.map((ve) => `${ve}`).join(',')}]`
}
if (RENDER_TYPE_MAP[key] === RENDER_TYPE.JSON_STRING) {
return `${key}: ${JSON.stringify(v)}`
}
if (RENDER_TYPE_MAP[key] === RENDER_TYPE.RAW) {
return `${key}: ${v}`
}
return ''
})
.join(',')

return optionsStrings
}
Loading

0 comments on commit c608fa9

Please sign in to comment.