Skip to content

Commit

Permalink
make it possible to passthrough functions to the swagger-initializer.…
Browse files Browse the repository at this point in the history
…js (#37)

* make it possible to passthrough functions to the swagger-initializer.js

* Apply suggestions from code review

Co-authored-by: Frazer Smith <frazer.dev@outlook.com>

* add unit tests, add Map and Set support

* improve typings and fix readme.md

* Update README.md

* Update README.md

Co-authored-by: Frazer Smith <frazer.dev@outlook.com>

* add comments ot initOAuth

* simplify swaggerInitializer

* Update lib/routes.js

Co-authored-by: Manuel Spigolon <behemoth89@gmail.com>

* unnamed symbols should also be serializable

* improve documentation regarding uiConfig

* add more tests

* rename file

---------

Co-authored-by: Frazer Smith <frazer.dev@outlook.com>
Co-authored-by: Manuel Spigolon <behemoth89@gmail.com>
  • Loading branch information
3 people authored Feb 17, 2023
1 parent 96d715e commit e2daa82
Show file tree
Hide file tree
Showing 10 changed files with 741 additions and 262 deletions.
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ await fastify.ready()
| transformStaticCSP | undefined | Synchronous function to transform CSP header for static resources if the header has been previously set. |
| transformSpecification | undefined | Synchronous function to transform the swagger document. |
| transformSpecificationClone| true | Provide a deepcloned swaggerObject to transformSpecification |
| uiConfig | {} | Configuration options for [Swagger UI](https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/configuration.md). Must be literal values, see [#5710](https://github.com/swagger-api/swagger-ui/issues/5710). |
| uiConfig | {} | Configuration options for [Swagger UI](https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/configuration.md). |
| uiHooks | {} | Additional hooks for the documentation's routes. You can provide the `onRequest` and `preHandler` hooks with the same [route's options](https://www.fastify.io/docs/latest/Routes/#options) interface.|
| logLevel | info | Allow to define route log level. |

Expand All @@ -121,6 +121,32 @@ The plugin will expose the documentation with the following APIs:
| `'/documentation/'` | The swagger UI |
| `'/documentation/*'` | External files that you may use in `$ref` |

#### uiConfig

To configure Swagger UI, you need to modify the `uiConfig` option.
It's important to ensure that functions are self-contained. Keep in mind that
you cannot modify the backend code within the `uiConfig` functions, as these
functions are processed only by the browser. You can reference the Swagger UI
element using `ui`, which is assigned to `window.ui`.

##### Example
```js
const fastify = require('fastify')()

await fastify.register(require('@fastify/swagger'))

await fastify.register(require('@fastify/swagger-ui'), {
uiConfig: {
onComplete: function () {
alert('ui has type of ' + typeof ui) // 'ui has type of object'
alert('fastify has type of ' + typeof fastify) // 'fastify has type of undefined'
alert('window has type of ' + typeof window) // 'window has type of object'
alert('global has type of ' + typeof global) // 'global has type of undefined'
}
}
})
```

#### transformSpecification

There can be use cases, where you want to modify the swagger definition on request. E.g. you want to modify the server
Expand Down
17 changes: 6 additions & 11 deletions lib/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const path = require('path')
const yaml = require('yaml')
const fastifyStatic = require('@fastify/static')
const rfdc = require('rfdc')()
const swaggerInitializer = require('./swagger-initializer')

// URI prefix to separate static assets for swagger UI
const staticPrefix = '/static'
Expand Down Expand Up @@ -74,23 +75,17 @@ function fastifySwagger (fastify, opts, done) {
}
})

fastify.route({
url: '/uiConfig',
method: 'GET',
schema: { hide: true },
...hooks,
handler: (req, reply) => {
reply.send(opts.uiConfig)
}
})
const swaggerInitializerContent = swaggerInitializer(opts)

fastify.route({
url: '/initOAuth',
url: `${staticPrefix}/swagger-initializer.js`,
method: 'GET',
schema: { hide: true },
...hooks,
handler: (req, reply) => {
reply.send(opts.initOAuth)
reply
.header('content-type', 'application/javascript; charset=utf-8')
.send(swaggerInitializerContent)
}
})

Expand Down
69 changes: 69 additions & 0 deletions lib/serialize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
'use strict'

function serialize (value) {
switch (typeof value) {
case 'bigint':
return value.toString() + 'n'
case 'boolean':
return value ? 'true' : 'false'
case 'function':
return value.toString()
case 'number':
return '' + value
case 'object':
if (value === null) {
return 'null'
} else if (Array.isArray(value)) {
return serializeArray(value)
} else if (value instanceof RegExp) {
return `/${value.source}/${value.flags}`
} else if (value instanceof Date) {
return `new Date(${value.getTime()})`
} else if (value instanceof Set) {
return `new Set(${serializeArray(Array.from(value))})`
} else if (value instanceof Map) {
return `new Map(${serializeArray(Array.from(value))})`
} else {
return serializeObject(value)
}
case 'string':
return JSON.stringify(value)
case 'symbol':
return serializeSymbol(value)
case 'undefined':
return 'undefined'
}
}
const symbolRE = /Symbol\((.+)\)/
function serializeSymbol (value) {
return symbolRE.test(value.toString())
? `Symbol("${value.toString().match(symbolRE)[1]}")`
: 'Symbol()'
}

function serializeArray (value) {
let result = '['
const il = value.length
const last = il - 1
for (let i = 0; i < il; ++i) {
result += serialize(value[i])
i !== last && (result += ',')
}
return result + ']'
}

function serializeObject (value) {
let result = '{'
const keys = Object.keys(value)
let i = 0
const il = keys.length
const last = il - 1
for (; i < il; ++i) {
const key = keys[i]
result += `"${key}":${serialize(value[key])}`
i !== last && (result += ',')
}
return result + '}'
}

module.exports = serialize
36 changes: 36 additions & 0 deletions lib/swagger-initializer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use strict'

const serialize = require('./serialize')

function swaggerInitializer (opts) {
return `window.onload = function () {
function resolveUrl(url) {
const anchor = document.createElement('a')
anchor.href = url
return anchor.href
}
const config = ${serialize(opts.uiConfig)}
const resConfig = Object.assign({}, {
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
}, config, {
url: resolveUrl('./json').replace('static/json', 'json'),
oauth2RedirectUrl: resolveUrl('./oauth2-redirect.html')
});
const ui = SwaggerUIBundle(resConfig)
window.ui = ui
ui.initOAuth(${serialize(opts.initOAuth)})
}`
}

module.exports = swaggerInitializer
50 changes: 0 additions & 50 deletions scripts/prepare-swagger-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ const filesToCopy = [
'index.html',
'index.css',
'oauth2-redirect.html',
'swagger-initializer.js',
'swagger-ui-bundle.js',
'swagger-ui-bundle.js.map',
'swagger-ui-standalone-preset.js',
Expand All @@ -29,55 +28,6 @@ filesToCopy.forEach(filename => {
fse.copySync(`${swaggerUiAssetPath}/${filename}`, resolve(`./static/${filename}`))
})

fse.writeFileSync(resolve(`./${folderName}/swagger-initializer.js`), `window.onload = function () {
function resolveUrl (url) {
const anchor = document.createElement('a')
anchor.href = url
return anchor.href
}
function resolveConfig (cb) {
return fetch(
resolveUrl('./uiConfig').replace('${folderName}/uiConfig', 'uiConfig')
)
.then(res => res.json())
.then((config) => {
const resConfig = Object.assign({}, {
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
}, config, {
url: resolveUrl('./json').replace('${folderName}/json', 'json'),
oauth2RedirectUrl: resolveUrl('./oauth2-redirect.html')
});
return cb(resConfig);
})
}
// Begin Swagger UI call region
const buildUi = function (config) {
const ui = SwaggerUIBundle(config)
window.ui = ui
fetch(resolveUrl('./initOAuth').replace('${folderName}/initOAuth', 'initOAuth'))
.then(res => res.json())
.then((config) => {
ui.initOAuth(config);
});
}
// End Swagger UI call region
resolveConfig(buildUi);
}`)

const sha = {
script: [],
style: []
Expand Down
67 changes: 2 additions & 65 deletions test/route.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,62 +74,6 @@ test('/documentation/json route', async (t) => {
t.pass('valid swagger object')
})

test('/documentation/uiConfig route', async (t) => {
t.plan(1)
const fastify = Fastify()

const uiConfig = {
docExpansion: 'full'
}

await fastify.register(fastifySwagger, swaggerOption)
await fastify.register(fastifySwaggerUi, { uiConfig })

fastify.get('/', () => {})
fastify.post('/', () => {})
fastify.get('/example', schemaQuerystring, () => {})
fastify.post('/example', schemaBody, () => {})
fastify.get('/parameters/:id', schemaParams, () => {})
fastify.get('/example1', schemaSecurity, () => {})

const res = await fastify.inject({
method: 'GET',
url: '/documentation/uiConfig'
})

const payload = JSON.parse(res.payload)

t.match(payload, uiConfig, 'uiConfig should be valid')
})

test('/documentation/initOAuth route', async (t) => {
t.plan(1)
const fastify = Fastify()

const initOAuth = {
scopes: ['openid', 'profile', 'email', 'offline_access']
}

await fastify.register(fastifySwagger, swaggerOption)
await fastify.register(fastifySwaggerUi, { initOAuth })

fastify.get('/', () => {})
fastify.post('/', () => {})
fastify.get('/example', schemaQuerystring, () => {})
fastify.post('/example', schemaBody, () => {})
fastify.get('/parameters/:id', schemaParams, () => {})
fastify.get('/example1', schemaSecurity, () => {})

const res = await fastify.inject({
method: 'GET',
url: '/documentation/initOAuth'
})

const payload = JSON.parse(res.payload)

t.match(payload, initOAuth, 'initOAuth should be valid')
})

test('fastify.swagger should return a valid swagger yaml', async (t) => {
t.plan(3)
const fastify = Fastify()
Expand Down Expand Up @@ -313,7 +257,7 @@ test('with routePrefix: \'/\' should redirect to ./static/index.html', async (t)
})

test('/documentation/static/:file should send back the correct file', async (t) => {
t.plan(22)
t.plan(21)
const fastify = Fastify()

await fastify.register(fastifySwagger, swaggerOption)
Expand Down Expand Up @@ -360,14 +304,7 @@ test('/documentation/static/:file should send back the correct file', async (t)
url: '/documentation/static/swagger-initializer.js'
})
t.equal(typeof res.payload, 'string')
t.equal(res.headers['content-type'], 'application/javascript; charset=UTF-8')
t.equal(
readFileSync(
resolve(__dirname, '..', 'static', 'swagger-initializer.js'),
'utf8'
),
res.payload
)
t.equal(res.headers['content-type'], 'application/javascript; charset=utf-8')
t.ok(res.payload.indexOf('resolveUrl') !== -1)
}

Expand Down
Loading

0 comments on commit e2daa82

Please sign in to comment.