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
2 changes: 1 addition & 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). Functions should be self contained, as variables outside the functions are not working. You can access the swagger-ui element with `ui` as it is assigned to `window.ui`. |
Uzlopak marked this conversation as resolved.
Show resolved Hide resolved
| 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 Down
55 changes: 44 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 serialize = require('./serialize')

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

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

const swaggerInitializer = `window.onload = function () {
function resolveUrl(url) {
const anchor = document.createElement('a')
anchor.href = url
return anchor.href
}

function resolveConfig(cb) {
const config = ${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')
});
return cb(resConfig);
}

// Begin Swagger UI call region
const buildUi = function (config) {
const ui = SwaggerUIBundle(config)
window.ui = ui
ui.initOAuth(${initOAuth});
}
// End Swagger UI call region
resolveConfig(buildUi);
}`

fastify.route({
url: '/initOAuth',
url: '/static/swagger-initializer.js',
Uzlopak marked this conversation as resolved.
Show resolved Hide resolved
method: 'GET',
schema: { hide: true },
...hooks,
handler: (req, reply) => {
reply.send(opts.initOAuth)
reply
.header('content-type', 'application/javascript; charset=utf-8')
.send(swaggerInitializer)
}
})

Expand Down
68 changes: 68 additions & 0 deletions lib/serialize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
'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) {
const symbolName = value.toString().match(symbolRE)[1]
return `Symbol("${symbolName}")`
}

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
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