// ==UserScript==
// @name Swagger typescript helper
// @license MIT
// @version 0.0.7
// @description 一个帮助用户在 Swagger UI 中快速生成对应 Typescript 接口的脚本 / A script that assists users in quickly generating corresponding TypeScript interfaces in Swagger UI.
// @author Nauxscript
// @run-at document-start
// @match */swagger-ui/*
// @match */*/swagger-ui/*
// @match */*/*/swagger-ui/*
// @namespace Nauxscript
// @require https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.js
// ==/UserScript==
(function () {
'use strict';
let apiDocsResponse
let hook_fetch = window.fetch; //储存原始fetch
window.unsafeWindow.fetch = async function (...args) { //劫持fetch
if (args[0].endsWith('/api-docs')) {
return await hook_fetch(...args).then((oriRes) => {
let hookRes = oriRes.clone() //克隆原始response
hookRes.text().then(res => { //读取克隆response
try {
res = JSON.parse(res)
if (res?.components?.schemas && res?.paths) {
apiDocsResponse = res
console.log(apiDocsResponse)
setTimeout(() => {
init()
}, 200)
} else {
console.error('Have no dict');
}
} catch (error) {
console.error(error);
}
})
return oriRes //返回原始response
})
}
return hook_fetch(...args)
}
function setupContainer(container) {
const pathEle = $(container).find('.opblock-summary-path').first()
const methodEle = $(container).find('.opblock-summary-method').first()
const path = $(pathEle).data('path')
const method = $(methodEle).text()
if (!path) return
setTimeout(() => {
const dict = apiDocsResponse.paths?.[path]?.[method.toLowerCase()]
if (!dict)
return
//
if (dict.parameters) {
const requestBodyContainer = container.querySelectorAll('.opblock-section')
generateBtns(requestBodyContainer, {
path,
method,
type: 'parameters'
})
} else if (dict.requestBody) {
const requestBodyContainer = container.querySelectorAll('.opblock-description-wrapper')
generateBtns(requestBodyContainer, {
path,
method,
type: 'requestBody'
})
} else {
console.error('Error Dict');
}
// responses
const responseSchemaItems = container.querySelectorAll('tr.response')
generateBtns(responseSchemaItems, {
path,
method,
type: 'responses'
})
}, 200)
}
$(document).on('click', '.opblock', function(event) {
const container = event.currentTarget
if ($(container).hasClass('is-open')) return
setupContainer(container)
});
$(document).on('click', '.tab-item', function(event) {
const btn = event.currentTarget
if ($(btn).hasClass('active'))
return;
if ($(btn).hasClass('no-tab-trigger')) {
console.log('parameters');
const container = $(btn).closest('.opblock-section')
$(btn).addClass('active')
$(btn).prev().removeClass('active')
$(container).children('.parameters-container').children('.table-container').hide()
if ($(container).children('.parameters-container').has('.ts-container').length) {
$(container).children('.parameters-container').children('.ts-container').show()
} else {
const path = $(btn).data('path')
const method = $(btn).data('method')
const reqOrRes = $(btn).data('type')
const { schema, type } = getSchema(reqOrRes, {
path,
method,
})
const interfaceRaw = generateInterface(schema, 'HelloType', type)
const result = combineInterfaces(interfaceRaw)
if (!container) {
return console.error('no container');
}
const tsContainer = $(`
<div class="ts-container" style="padding: 20px;">
<div class="highlight-code">
<pre class="example microlight" style="display: block; overflow-x: auto; padding: 0.5em; background: rgb(51, 51, 51); color: white;">
<code style="white-space: pre;">
${result}
</code>
</pre>
</div>
</div>
`)
$(container).children('.parameters-container').append(tsContainer)
}
}
if ($(btn).next().hasClass('no-tab-trigger')) {
console.log('parameters interface');
const container = $(btn).closest('.opblock-section')
$(btn).addClass('active')
$(btn).next().removeClass('active')
$(container).children('.parameters-container').children('.table-container').show()
$(container).children('.parameters-container').children('.ts-container').hide()
}
})
$(document).on('click', '.ts-tab', function(event) {
const btn = event.currentTarget
const container = $(btn).closest('.model-example')
const codeContainer = $(container).children().last()
if ($(btn).hasClass('show')) {
$(container).children('.ts-container').hide()
$(btn).removeClass('show')
} else {
$(btn).addClass('show')
if ($(container).has('.ts-container').length) {
$(container).children('.ts-container').show()
} else {
const path = $(btn).data('path')
const method = $(btn).data('method')
const reqOrRes = $(btn).data('type')
const { schema, type } = getSchema(reqOrRes, {
path,
method,
})
const interfaceRaw = generateInterface(schema, 'HelloType', type)
const result = combineInterfaces(interfaceRaw)
if (!container) {
return console.error('no container');
}
const tsContainer = $(`
<div class="ts-container" style="margin-bottom: 5px">
<div class="highlight-code">
<pre class="example microlight" style="display: block; overflow-x: auto; padding: 0.5em; background: rgb(51, 51, 51); color: white;">
<code style="white-space: pre;">
${result}
</code>
</pre>
</div>
</div>
`)
$(codeContainer).before(tsContainer)
const copyBtn = $(btn).children('.copy-btn')
copyBtn.show()
$(copyBtn).on('click', function(e) {
e.stopPropagation()
copyToClipboard(result).then(function() {
$(copyBtn).css('color', 'green')
$(copyBtn).text('已复制')
resetCopyBtn(copyBtn)
}).catch(() => {
$(copyBtn).css('color', 'red')
$(copyBtn).text('复制失败')
resetCopyBtn(copyBtn)
})
})
function resetCopyBtn(ele) {
setTimeout(() => {
$(ele).css('color', 'inherit')
$(ele).text('点击复制')
}, 2000)
}
}
}
})
function getSchema(schemaType, { path, method }) {
if (!path) {
throw new Error('Wrong path')
}
if (schemaType === 'responses') {
const response = apiDocsResponse.paths?.[path]?.[method.toLowerCase()]?.responses
if (!response) {
throw new Error('No response model')
}
const schemaWrapper = getSchemaWarpper(response['200'])
const { type, ref } = normalizeSchema(schemaWrapper.schema)
if (!ref) {
throw new Error('No schemaRef')
}
const field = ref2field(ref)
const schema = apiDocsResponse.components.schemas[field]
return {
schema,
type
}
}
let schemaWrapper
if (schemaType === 'parameters') {
schemaWrapper = apiDocsResponse.paths?.[path]?.[method.toLowerCase()]?.[schemaType]
return {
type: 'object',
schema: generateParamStructure(schemaWrapper)
}
} else {
const requestBody = apiDocsResponse.paths?.[path]?.[method.toLowerCase()]?.[schemaType]
if (!requestBody) {
throw new Error('No requestBody model')
}
schemaWrapper = getSchemaWarpper(requestBody)
}
const { type, ref } = normalizeSchema(schemaWrapper.schema)
if (!ref) {
console.error('No schemaRef');
}
const field = ref2field(ref)
const schema = apiDocsResponse.components.schemas[field]
return {
schema,
type
}
}
function normalizeSchema(schemaRaw) {
if (schemaRaw.$ref) {
return {
type: 'object',
ref: schemaRaw.$ref
}
}
if (schemaRaw?.type === 'array') {
return {
type: schemaRaw.type,
ref: schemaRaw.items.$ref
}
}
if (schemaRaw?.type === 'string') {
return {
type: schemaRaw.type,
ref: schemaRaw.$ref
}
}
throw new Error('unexpected type', schemaRaw)
}
function init() {
$('.opblock.is-open').each(function(){
setupContainer(this)
})
}
function generateBtns(containers, {
path,
method,
type
}) {
containers.forEach(item => {
let btn, tab
if (type === 'parameters') {
btn = createParammeterTab(path, method, type)
tab = item.querySelector('.tab-header')
} else {
btn = createTab(path, method, type)
tab = item.querySelector('.tab')
}
tab?.appendChild(btn)
})
}
function createParammeterTab(path, method, type) {
return $(`
<div class="tab-item no-tab-trigger" data-path="${path}" data-method="${method}" data-type="${type}"><h4 class="opblock-title"><span>Parameters Interface</span></h4></div>
`)[0]
}
function createTab(path, method, type) {
const tabBtn = document.createElement('li')
tabBtn.classList.add('tabitem', 'ts-tab')
tabBtn.dataset.path = path
tabBtn.dataset.method = method
tabBtn.dataset.type = type
tabBtn.style.borderLeft = '1px solid rgba(0,0,0,.2)';
tabBtn.style.paddingLeft = '6px';
const innerALabel = document.createElement('a')
innerALabel.innerText = 'TS Interface'
tabBtn.append(innerALabel)
const copyBtn = document.createElement('a')
copyBtn.classList.add('copy-btn')
copyBtn.innerText = '点击复制'
copyBtn.style.display = "none";
copyBtn.style.padding = "0 4px";
tabBtn.append(copyBtn)
return tabBtn
}
function generateInterface(json, name, currentType) {
if (!json) return 'any'
const dep = []
let result = generateSarter(currentType, name)
for (const key in json.properties) {
const property = json.properties[key]
let type = property.type;
if (type === undefined) {
// object
const ref = property.$ref
console.log(type, ref)
const innerTypeName = capitalizeFirstLetter(key) + 'Type'
const schema = apiDocsResponse.components.schemas[ref2field(ref)]
const data = generateInterface(schema, innerTypeName, 'object')
dep.push(data.result,...data.dep)
type = innerTypeName;
} else if (type === 'array') {
const ref = property.items?.$ref
const fieldType = property.items?.type
console.log(type, ref)
if (ref) {
const innerTypeName = capitalizeFirstLetter(key) + 'Row'
const schema = apiDocsResponse.components.schemas[ref2field(ref)]
const data = generateInterface(schema, innerTypeName, 'object')
dep.push(data.result, ...data.dep)
type = `${innerTypeName}[]`;
} else if (fieldType) {
type = `${fieldType === 'integer' ? 'number' : fieldType}[]`
}
} else if (type === 'integer') {
type = 'number'
} else if (type === 'string' && property.enum) {
type = name + capitalizeFirstLetter(key)
dep.push(generateEnum(type, property.enum))
}
result += ` ${key}: ${type};\n`;
}
if (currentType === 'object') {
result += '}\n'
} else {
result += '[]'
}
return {result, dep};
}
function combineInterfaces(raw) {
const depStr = raw.dep.reduce((prev, curr) => {
prev += curr
return prev
}, '')
return raw.result + depStr
}
function ref2field(ref) {
return ref.replace('#/components/schemas/', '')
}
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
function generateSarter(type, name) {
if (type === 'object') {
return `\ninterface ${name} {\n`;
}
if (type === 'array') {
return `\ntype ${name} = `
}
if (type === 'enum') {
return `\ntype ${name} = `
}
}
/**
* @author: tzx
* @description:
* @param { string } name
* @param { Array<string> } enums
*/
function generateEnum(name, enums = []) {
if (!enums.length || !name)
return ''
return generateSarter('enum', name) + enums.map(s => `'${s}'`).join(' | ') + '\n'
}
function copyToClipboard(text) {
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(text)
} else {
let textArea = document.createElement('textarea')
textArea.value = text
textArea.style.position = "absolute"
textArea.style.opacity = "0"
textArea.style.left = "-999999px"
textArea.style.top = window.scrollY + 'px'
document.body.append(textArea)
textArea.focus()
textArea.select()
return new Promise((resolve, reject) => {
document.execCommand('copy') ? resolve() : reject()
textArea.remove()
})
}
}
function getSchemaWarpper(rawBody) {
const allAccept = '*/*'
const jsonAccept = 'application/json'
const content = rawBody.content
if (!content && typeof content !== 'object') {
throw new Error('No Content')
}
const innerKey = Object.keys(content).find(key => key.includes(allAccept) || key.includes(jsonAccept))
const schemaWrapper = content[innerKey]
if (!schemaWrapper)
throw new Error('No Schema')
return schemaWrapper
}
function generateParamStructure(parameters) {
if (!(parameters instanceof Array)) {
throw new Error('Parameters Schema Error')
}
return parameters.reduce((prev, curr) => {
if (curr.schema.type) {
prev.properties[curr.name] = {
type: curr.schema.type
}
} else if (curr.schema.$ref) {
prev.properties[curr.name] = {
$ref: curr.schema.$ref
}
}
return prev
}, {
properties: {}
})
}
})()