Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 13 commits into from
Feb 17, 2023
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