@openapi-generator-plus/plain-documentation-generator
Version:
An OpenAPI Generator Plus template for generating plain documentation
303 lines (270 loc) • 9.56 kB
text/typescript
import { CodegenAllOfStrategy, CodegenAnyOfStrategy, CodegenGeneratorConstructor, CodegenGeneratorType, CodegenOneOfStrategy, isCodegenOperation } from '@openapi-generator-plus/types'
import { CodegenOptionsDocumentation } from './types'
import path from 'path'
import Handlebars from 'handlebars'
import { loadTemplates, emit, registerStandardHelpers } from '@openapi-generator-plus/handlebars-templates'
import { javaLikeGenerator, JavaLikeContext, ConstantStyle, options as javaLikeOptions, EnumMemberStyle } from '@openapi-generator-plus/java-like-generator-helper'
import { commonGenerator, compareHttpMethods, configObject, configString } from '@openapi-generator-plus/generator-common'
import { emit as emitLess } from './less-utils'
import { copyContents } from './static-utils'
import { existsSync, promises as fs } from 'fs'
function computeCustomTemplatesPath(configPath: string | undefined, customTemplatesPath: string) {
if (configPath) {
return path.resolve(path.dirname(configPath), customTemplatesPath)
} else {
return customTemplatesPath
}
}
function toSafeTypeForComposing(nativeType: string): string {
if (/[^a-zA-Z0-9_.[\]]/.test(nativeType)) {
return `(${nativeType})`
} else {
return nativeType
}
}
export const createGenerator: CodegenGeneratorConstructor = (config, context) => {
const javaLikeContext: JavaLikeContext = {
...context,
defaultConstantStyle: ConstantStyle.allCapsSnake,
defaultEnumMemberStyle: EnumMemberStyle.preserve,
}
const customTemplates = configString(config, 'customTemplates', undefined)
const operationsConfig = configObject<CodegenOptionsDocumentation['operations']>(config, 'operations', {})!
if (operationsConfig.navStyle === undefined) {
operationsConfig.navStyle = 'name'
}
if (operationsConfig.exclude === undefined) {
operationsConfig.exclude = []
}
const generatorOptions: CodegenOptionsDocumentation = {
...javaLikeOptions(config, javaLikeContext),
customTemplatesPath: customTemplates && computeCustomTemplatesPath(config.configPath, customTemplates),
operations: operationsConfig,
}
const aCommonGenerator = commonGenerator(config, context)
return {
...context.baseGenerator(config, context),
...aCommonGenerator,
...javaLikeGenerator(config, javaLikeContext),
generatorType: () => CodegenGeneratorType.DOCUMENTATION,
toIdentifier: (name) => name,
toLiteral: (value, options) => {
if (value === undefined) {
const defaultValue = context.generator().defaultValue(options)
if (defaultValue === null) {
return null
}
return defaultValue.literalValue
}
return `${value}`
},
toNativeType: (options) => {
const { type, format } = options
if (type === 'string') {
if (format) {
return new context.NativeType(format, {
serializedType: 'string',
})
}
} else if (type === 'integer') {
if (format) {
return new context.NativeType(format, {
serializedType: 'number',
})
} else {
return new context.NativeType(type, {
serializedType: 'number',
})
}
}
return new context.NativeType(type)
},
toEnumMemberName: (name) => name,
toNativeObjectType: function(options) {
const { scopedName } = options
let modelName = ''
for (const name of scopedName) {
modelName += `.${context.generator().toClassName(name)}`
}
return new context.NativeType(modelName.substring(1))
},
toNativeArrayType: (options) => {
const { componentNativeType } = options
return new context.NativeType(`${toSafeTypeForComposing(componentNativeType.nativeType)}[]`)
},
toNativeMapType: (options) => {
const { keyNativeType, componentNativeType } = options
return new context.NativeType(`{ [name: ${keyNativeType}]: ${componentNativeType} }`)
},
nativeTypeUsageTransformer: ({ nullable }) => ({
default: function(nativeType, nativeTypeString) {
if (nullable) {
return `${toSafeTypeForComposing(nativeTypeString)} | null`
}
return nativeTypeString
},
/* We don't transform the concrete type as the concrete type is never null; we use it to make new objects */
concreteType: null,
}),
defaultValue: () => {
return null
},
initialValue: () => {
return null
},
toOperationGroupName: (name) => {
return name
},
operationGroupingStrategy: () => {
return context.operationGroupingStrategies.addToGroupsByTagOrPath
},
allOfStrategy: () => CodegenAllOfStrategy.NATIVE,
anyOfStrategy: () => CodegenAnyOfStrategy.NATIVE,
oneOfStrategy: () => CodegenOneOfStrategy.NATIVE,
supportsInheritance: () => false,
supportsMultipleInheritance: () => false,
nativeCompositionCanBeScope: () => true,
nativeComposedSchemaRequiresName: () => false,
nativeComposedSchemaRequiresObjectLikeOrWrapper: () => false,
interfaceCanBeNested: () => true,
watchPaths: () => {
const result = [path.resolve(__dirname, '..', 'templates')]
result.push(path.resolve(__dirname, '..', 'less'))
result.push(path.resolve(__dirname, '..', 'static'))
if (generatorOptions.customTemplatesPath) {
result.push(generatorOptions.customTemplatesPath)
}
return result
},
cleanPathPatterns: () => {
return [
'index.html',
'static/**',
'main.css',
'custom.css',
]
},
templateRootContext: () => {
return {
...aCommonGenerator.templateRootContext(),
...generatorOptions,
generatorClass: '@openapi-generator-plus/plain-documentation-generator',
}
},
postProcessDocument(doc) {
/* Apply excludes on operations */
for (let i = 0; i < doc.groups.length; i++) {
const group = doc.groups[i]
for (let j = 0; j < group.operations.length; j++) {
const op = group.operations[j]
for (const exclude of generatorOptions.operations?.exclude || []) {
if (op.fullPath.match(new RegExp(exclude))) {
group.operations.splice(j, 1)
j--
break
}
}
}
if (group.operations.length === 0) {
doc.groups.splice(i, 1)
i--
}
}
},
exportTemplates: async(outputPath, doc) => {
const hbs = Handlebars.create()
registerStandardHelpers(hbs, context)
hbs.registerHelper('eachSorted', function(this: unknown, context: unknown, options: Handlebars.HelperOptions) {
if (!context) {
return options.inverse(this)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let collection: any[]
if (context instanceof Map) {
collection = [...context.values()]
} else if (Array.isArray(context)) {
collection = context
} else if (typeof context === 'object') {
collection = []
for (const key in context) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
collection.push((context as any)[key])
}
} else {
collection = [context]
}
let result = ''
// eslint-disable-next-line @typescript-eslint/no-explicit-any
for (const item of collection.sort(function(a: any, b: any) {
if (a === b) {
return 0
}
if (typeof a === 'string' && typeof b === 'string') {
return a.localeCompare(b)
} else if (typeof a === 'number' && typeof b === 'number') {
return a < b ? -1 : a > b ? 1 : 0
} else if (typeof a === 'object' && typeof b === 'object') {
if (a === null) {
return 1
} else if (b === null) {
return -1
}
if (isCodegenOperation(a) && isCodegenOperation(b)) {
/* Sort CodegenOperations first by http method */
const result = compareHttpMethods(a.httpMethod, b.httpMethod)
if (result !== 0) {
return result
}
if (generatorOptions.operations?.navStyle === 'full-path') {
if (a.fullPath && b.fullPath) {
return a.fullPath.localeCompare(b.fullPath)
}
}
}
if (a.name && b.name) {
return a.name.localeCompare(b.name)
}
return 0
} else {
return 0
}
})) {
result += options.fn(item)
}
return result
})
hbs.registerHelper('htmlId', function(value: string) {
if (value !== undefined) {
return `${value}`.replace(/[^-a-zA-Z0-9_]+/g, '_').replace(/^_+/, '').replace(/_+$/, '')
} else {
return value
}
})
await loadTemplates(path.resolve(__dirname, '..', 'templates'), hbs)
if (generatorOptions.customTemplatesPath) {
await loadTemplates(generatorOptions.customTemplatesPath, hbs)
}
const rootContext = context.generator().templateRootContext()
if (!outputPath.endsWith('/')) {
outputPath += '/'
}
await emit('index', path.join(outputPath, 'index.html'), { ...rootContext, ...doc }, true, hbs)
await emitLess(path.resolve(__dirname, '../less', 'style.less'), path.join(outputPath, 'static/css/main.css'))
await fs.writeFile(path.join(outputPath, 'custom.css'), '', {})
if (generatorOptions.customTemplatesPath) {
const customLessPath = path.resolve(generatorOptions.customTemplatesPath, 'less/custom.less')
if (existsSync(customLessPath)) {
await emitLess(customLessPath, path.join(outputPath, 'static/css/custom.css'))
}
}
await copyContents(path.resolve(__dirname, '..', 'static'), path.join(outputPath, 'static'))
if (generatorOptions.customTemplatesPath) {
const customStaticPath = path.resolve(generatorOptions.customTemplatesPath, 'static')
if (existsSync(customStaticPath)) {
await copyContents(customStaticPath, path.join(outputPath, 'static'))
}
}
},
}
}
export default createGenerator