import jsesc from 'jsesc' ; import fs from 'fs-extra' ; import path from 'path' ; import { camelCase , deburr , kebabCase , upperCase , upperFirst } from 'lodash' ; import { OpenAPIObject , ReferenceObject , SchemaObject } from 'openapi3-ts' ; import { Options } from './options' ; import { Model } from './model' ; export const HTTP_METHODS = [ 'get' , 'put' , 'post' , 'delete' , 'options' , 'head' , 'patch' , 'trace' ] ; type SchemaOrRef = SchemaObject | ReferenceObject ; /**
 * Returns the simple name, that is, the last part after '/'
 */ export function simpleName ( name ) { const pos = name . lastIndexOf ( '/' ) ; return name . substring ( pos + 1 ) ; } /**
 * Returns the unqualified model class name, that is, the last part after '.'
 */ export function unqualifiedName ( name , options ) { const pos = name . lastIndexOf ( '.' ) ; return modelClass ( name . substring ( pos + 1 ) , options ) ; } /**
 * Returns the qualified model class name, that is, the camelized namespace (if any) plus the unqualified name
 */ export function qualifiedName ( name , options ) { const ns = namespace ( name ) ; const unq = unqualifiedName ( name , options ) ; return ns ? typeName ( ns ) + unq : unq ; } /**
 * Returns the file to import for a given model
 */ export function modelFile ( pathToModels , name , options ) { let dir = pathToModels || '' ; if ( dir . endsWith ( '/' ) ) { dir = dir . substr ( 0 , dir . length - 1 ) ; } const ns = namespace ( name ) ; if ( ns ) { dir += `/${ ns }` ; } const file = unqualifiedName ( name , options ) ; return dir += '/' + fileName ( file ) ; } /**
 * Returns the namespace path, that is, the part before the last '.' split by '/' instead of '.'.
 * If there's no namespace, returns undefined.
 */ export function namespace ( name ) { name = name . replace ( /^\.+/g , '' ) ; name = name . replace ( /\.+$/g , '' ) ; const pos = name . lastIndexOf ( '.' ) ; return pos < 0 ? undefined : name . substring ( 0 , pos ) . replace ( /\./g , '/' ) ; } /**
 * Returns the type (class) name for a given regular name
 */ export function typeName ( name ) { return upperFirst ( methodName ( name ) ) ; } /**
 * Returns the name of the enum constant for a given value
 */ export function enumName ( value , options ) { let name = toBasicChars ( value , true ) ; if ( options . enumStyle === 'upper' ) { name = upperCase ( name ) . replace ( /\s+/g , '_' ) ; } else { name = upperFirst ( camelCase ( name ) ) ; } if ( /^\d/ . test ( name ) ) { name = '$' + name ; } return name ; } param name export function methodName ( name ) { return camelCase ( toBasicChars ( name , true ) ) ; } /**
 * Returns the file name for a given type name
 */ export function fileName ( text ) { return kebabCase ( toBasicChars ( text ) ) ; } /**
 * Converts a text to a basic, letters / numbers / underscore representation.
 * When firstNonDigit is true, prepends the result with an uderscore if the first char is a digit.
 */ export function toBasicChars ( text , firstNonDigit = false ) { text = deburr ( ( text || '' ) . trim ( ) ) ; text = text . replace ( /[^\w]+/g , '_' ) ; if ( firstNonDigit && /[0-9]/ . test ( text . charAt ( 0 ) ) ) { text = '_' + text ; } return text ; } /**
 * Returns the TypeScript comments for the given schema description, in a given indentation level
 */ export function tsComments ( description , level , deprecated ? ) { const indent = '  ' . repeat ( level ) ; if ( description == undefined || description . length === 0 ) { return indent + ( deprecated ? '/** @deprecated */' : '' ) ; } const lines = description . trim ( ) . split ( '\n' ) ; let result = '\n' + indent + '/**\n' ; lines . forEach ( line => { result += indent + ' *' + ( line === '' ? '' : ' ' + line . replace ( /\*\//g , '* / ' ) ) + '\n' ; } ) ; if ( deprecated ) { result += indent + ' *\n' + indent + ' * @deprecated\n' ; } result += indent + ' */\n' + indent ; return result ; } /**
 * Applies the prefix and suffix to a model class name
 */ export function modelClass ( baseName , options ) { return `${ options . modelPrefix || '' }${ typeName ( baseName ) }${ options . modelSuffix || '' }` ; } /**
 * Applies the prefix and suffix to a service class name
 */ export function serviceClass ( baseName , options ) { return `${ options . servicePrefix || '' }${ typeName ( baseName ) }${ options . serviceSuffix || 'Service' }` ; } /**
 * Escapes the name of a property / parameter if not valid JS identifier
 */ export function escapeId ( name ) { if ( /^[a-zA-Z]\w+$/ . test ( name ) ) { return name ; } else { return `'${ name . replace ( /\'/g , '\\\'' ) }'` ; } } /**
 * Returns the TypeScript type for the given type and options
 */ export function tsType ( schemaOrRef , options , openApi , container ? ) { if ( ! schemaOrRef ) { return 'any' ; } if ( schemaOrRef . $ref ) { const resolved = resolveRef ( openApi , schemaOrRef . $ref ) ; const nullable = ! ! ( resolved && ( resolved as SchemaObject ) . nullable ) ; const prefix = nullable ? 'null | ' : '' ; const name = simpleName ( schemaOrRef . $ref ) ; if ( container && container . name === name ) { return prefix + container . typeName ; } else { return prefix + qualifiedName ( name , options ) ; } } const schema = schemaOrRef as SchemaObject ; const union = schema . oneOf || schema . anyOf || [ ] ; if ( union . length > 0 ) { if ( union . length > 1 ) { return `(${ union . map ( u => tsType ( u , options , openApi , container ) ) . join ( ' | ' ) })` ; } else { return union . map ( u => tsType ( u , options , openApi , container ) ) . join ( ' | ' ) ; } } const type = schema . type || 'any' ; if ( type === 'array' || schema . items ) { return `Array<${ tsType ( schema . items || { } , options , openApi , container ) }>` ; } const allOf = schema . allOf || [ ] ; let intersectionType = [ ] ; if ( allOf . length > 0 ) { intersectionType = allOf . map ( u => tsType ( u , options , openApi , container ) ) ; } if ( type === 'object' || schema . properties ) { let result = '{\n' ; const properties = schema . properties || { } ; const required = schema . required ; for ( const propName of Object . keys ( properties ) ) { const property = properties [ propName ] ; if ( ! property ) { continue ; } if ( ( property as SchemaObject ) . description ) { result += tsComments ( ( property as SchemaObject ) . description , 0 , ( property as SchemaObject ) . deprecated ) ; } result += `'${ propName }'` ; const propRequired = required && required . includes ( propName ) ; if ( ! propRequired ) { result += '?' ; } let propertyType = tsType ( property , options , openApi , container ) ; if ( ( property as SchemaObject ) . nullable ) { propertyType = `${ propertyType } | null` ; } result += `: ${ propertyType };\n` ; } if ( schema . additionalProperties ) { const additionalProperties = schema . additionalProperties === true ? { } : schema . additionalProperties ; result += `[key: string]: ${ tsType ( additionalProperties , options , openApi , container ) };\n` ; } result += '}' ; intersectionType . push ( result ) ; } if ( intersectionType . length > 0 ) { return intersectionType . join ( ' & ' ) ; } const enumValues = schema . enum || [ ] ; if ( enumValues . length > 0 ) { if ( type === 'number' || type === 'integer' || type === 'boolean' ) { return enumValues . join ( ' | ' ) ; } else { return enumValues . map ( v => `'${ jsesc ( v ) }'` ) . join ( ' | ' ) ; } } if ( type === 'string' && schema . format === 'binary' ) { return 'Blob' ; } return type === 'integer' ? 'number' : type ; } param ref export function resolveRef ( openApi , ref ) { if ( ! ref . includes ( '/' ) ) { ref = `#/components/schemas/${ ref }` ; } let current = null ; for ( let part of ref . split ( '/' ) ) { part = part . trim ( ) ; if ( part === '#' || part === '' ) { current = openApi ; } else if ( current == null ) { break ; } else { current = current [ part ] ; } } if ( current == null || typeof current !== 'object' ) { throw new Error ( `Couldn't resolve reference ${ ref }` ) ; } return current as SchemaObject ; } /**
 * Recursively deletes a directory
 */ export function deleteDirRecursive ( dir ) { if ( fs . existsSync ( dir ) ) { fs . readdirSync ( dir ) . forEach ( ( file ) => { const curPath = path . join ( dir , file ) ; if ( fs . lstatSync ( curPath ) . isDirectory ( ) ) { deleteDirRecursive ( curPath ) ; } else { fs . unlinkSync ( curPath ) ; } } ) ; fs . rmdirSync ( dir ) ; } } /**
 * Synchronizes the files from the source to the target directory. Optionally remove stale files.
 */ export function syncDirs ( srcDir , destDir , removeStale ) { fs . ensureDirSync ( destDir ) ; const srcFiles = fs . readdirSync ( srcDir ) ; const destFiles = fs . readdirSync ( destDir ) ; for ( const file of srcFiles ) { const srcFile = path . join ( srcDir , file ) ; const destFile = path . join ( destDir , file ) ; if ( fs . lstatSync ( srcFile ) . isDirectory ( ) ) { syncDirs ( srcFile , destFile , removeStale ) ; } else { const srcContent = fs . readFileSync ( srcFile , { encoding : 'utf-8' } ) ; const destContent = fs . existsSync ( destFile ) ? fs . readFileSync ( destFile , { encoding : 'utf-8' } ) : null ; if ( srcContent !== destContent ) { fs . writeFileSync ( destFile , srcContent , { encoding : 'utf-8' } ) ; console . debug ( 'Wrote ' + destFile ) ; } } } if ( removeStale ) { for ( const file of destFiles ) { const srcFile = path . join ( srcDir , file ) ; const destFile = path . join ( destDir , file ) ; if ( ! fs . existsSync ( srcFile ) && fs . lstatSync ( destFile ) . isFile ( ) ) { fs . unlinkSync ( destFile ) ; console . debug ( 'Removed stale file ' + destFile ) ; } } } }