@sphereon/ssi-sdk.dev
Version:
193 lines (156 loc) • 6.8 kB
text/typescript
import { Extractor, ExtractorConfig, ExtractorResult } from '@microsoft/api-extractor'
import { ApiMethodSignature, ApiModel, ApiParameterListMixin, ApiReturnTypeMixin } from '@microsoft/api-extractor-model'
import { Command } from 'commander'
import { writeFileSync } from 'fs'
import { OpenAPIV3 } from 'openapi-types'
import { resolve } from 'path'
import * as TJS from 'ts-json-schema-generator'
import module from 'module'
const requireCjs = module.createRequire(import.meta.url)
interface Method {
packageName: string
pluginInterfaceName: string
operationId: string
description?: string
parameters?: string
response: string
}
const genericTypes = ['boolean', 'string', 'number', 'any', 'Array<string>']
function createSchema(generator: TJS.SchemaGenerator, symbol: string) {
if (genericTypes.includes(symbol)) {
return { components: { schemas: {} } }
}
const fixedSymbol = symbol.replace(/Array\<(.*)\>/gm, '$1').replace(/(\\:[\w]?Certificate)/gm, ': any')
const schema = generator.createSchema(fixedSymbol)
const newSchema = {
components: {
schemas: schema.definitions,
},
}
let schemaStr = JSON.stringify(newSchema, null, 2)
schemaStr = schemaStr.replace(/#\/definitions\//gm, '#/components/schemas/')
schemaStr = schemaStr.replace(/\"patternProperties\":{([^:]*):{[^}]*}}/gm, '"pattern": $1')
schemaStr = schemaStr.replace(/Verifiable\<(.*)\>/gm, 'Verifiable-$1')
schemaStr = schemaStr.replace(/Where\<(.*)\>/gm, 'Where-$1')
schemaStr = schemaStr.replace(/Order\<(.*)\>/gm, 'Order-$1')
schemaStr = schemaStr.replace(/FindArgs\<(.*)\>/gm, 'FindArgs-$1')
schemaStr = schemaStr.replace(/https \:\/\//gm, 'https://')
// a bug in the schema generator stack mangles @link tags with text.
schemaStr = schemaStr.replace(/\{@link\s+([^|}]+?)\s([^|}]+)\s}/g, '{@link $1 | $2 }')
return JSON.parse(schemaStr)
}
function getReference(response: string): OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject {
if (!response) {
return { type: 'object' }
}
if (response.slice(0, 6) === 'Array<') {
const symbol = response.replace('Array<', '').replace('>', '') as 'string' | 'number' | 'boolean' | 'object' | 'integer'
return {
type: 'array',
items: genericTypes.includes(symbol) ? { type: symbol } : { $ref: '#/components/schemas/' + symbol },
}
}
response = response.replace(/(\\:?[\w]*Certificate)/gm, 'any')
if (response === 'any') {
return { type: 'object' }
}
if (['string', 'number', 'boolean', 'object', 'integer'].includes(response)) {
// @ts-ignore
return { type: response }
} else {
return { $ref: '#/components/schemas/' + response }
}
}
const dev = new Command('dev').description('Plugin developer tools')
dev
.command('generate-plugin-schema')
.description('generate plugin schema')
.option('-c, --extractorConfig <string>', 'API Extractor config file', './api-extractor.json')
.option('-p, --packageConfig <string>', 'package.json file containing a Veramo plugin interface config', './package.json')
.action(async (options) => {
const apiExtractorJsonPath: string = resolve(options.extractorConfig)
const extractorConfig: ExtractorConfig = ExtractorConfig.loadFileAndPrepare(apiExtractorJsonPath)
const extractorResult: ExtractorResult = Extractor.invoke(extractorConfig, {
localBuild: true,
showVerboseMessages: true,
})
if (!extractorResult.succeeded) {
console.error(`API Extractor completed with ${extractorResult.errorCount} errors` + ` and ${extractorResult.warningCount} warnings`)
process.exitCode = 1
}
const packageConfig = requireCjs(resolve(options.packageConfig))
const interfaces: any = {}
for (const pluginInterfaceName in packageConfig?.veramo?.pluginInterfaces) {
const entryFile = packageConfig.veramo.pluginInterfaces[pluginInterfaceName]
const api = {
components: {
schemas: {},
methods: {},
},
}
const generator = TJS.createGenerator({
path: resolve(entryFile),
encodeRefs: false,
additionalProperties: true,
skipTypeCheck: true,
// functions: 'hide',
})
const apiModel: ApiModel = new ApiModel()
const apiPackage = apiModel.loadPackage(extractorConfig.apiJsonFilePath)
const entry = apiPackage.entryPoints[0]
const pluginInterface = entry.findMembersByName(pluginInterfaceName)[0]
for (const member of pluginInterface.members) {
const method: Partial<Method> = {}
method.pluginInterfaceName = pluginInterfaceName
method.operationId = member.displayName
// console.log(member)
method.parameters = (member as ApiParameterListMixin).parameters[0]?.parameterTypeExcerpt?.text
method.response = (member as ApiReturnTypeMixin).returnTypeExcerpt.text.replace('Promise<', '').replace('>', '')
const methodSignature = member as ApiMethodSignature
method.description = methodSignature.tsdocComment?.summarySection
?.getChildNodes()[0]
// @ts-ignore
?.getChildNodes()[0]?.text
method.description = method.description || ''
if (method.parameters) {
// @ts-ignore
api.components.schemas = {
// @ts-ignore
...api.components.schemas,
...createSchema(generator, method.parameters).components.schemas,
}
}
// @ts-ignore
api.components.schemas = {
// @ts-ignore
...api.components.schemas,
...createSchema(generator, method.response).components.schemas,
}
// @ts-ignore
api.components.methods[method.operationId] = {
description: method.description,
arguments: getReference(method.parameters),
returnType: getReference(method.response),
}
}
interfaces[pluginInterfaceName] = api
}
writeFileSync(resolve('./plugin.schema.json'), JSON.stringify(interfaces, null, 2))
})
dev
.command('extract-api')
.description('Extract API')
.option('-c, --extractorConfig <string>', 'API Extractor config file', './api-extractor.json')
.action(async (options) => {
const apiExtractorJsonPath: string = resolve(options.extractorConfig)
const extractorConfig: ExtractorConfig = ExtractorConfig.loadFileAndPrepare(apiExtractorJsonPath)
const extractorResult: ExtractorResult = Extractor.invoke(extractorConfig, {
localBuild: true,
showVerboseMessages: true,
})
if (!extractorResult.succeeded) {
console.error(`API Extractor completed with ${extractorResult.errorCount} errors` + ` and ${extractorResult.warningCount} warnings`)
process.exitCode = 1
}
})
export { dev }