UNPKG

@codegena/oapi3ts

Version:

Codegeneration from OAS3 to TypeScript

1,302 lines (1,288 loc) 92.5 kB
import { createSourceFile, ScriptTarget } from 'typescript'; import * as _ from 'lodash'; import ___default, { uniqBy, intersectionBy, flatten } from 'lodash'; import Ajv from 'ajv'; import * as jsonPointer from 'json-pointer'; import { Generic } from '@codegena/definitions/json-schema'; import { Oas3ParameterTarget } from '@codegena/definitions/oas3'; import prettier from 'prettier/standalone'; import prettierParserMD from 'prettier/parser-markdown'; import prettierParserTS from 'prettier/parser-typescript'; class ParsingProblems { static parsingWarning(message, meta) { if (this.throwErrorOnWarning) { throw new ParsingError(message, meta); } if (this.onWarnings) { this.onWarnings(message, meta); } console.warn(`WARNING: ${message}\n${(meta && meta.jsonPath) ? `JSON Path of problem place: ${meta.jsonPath}` : 'No json path attached.'}`); } } ParsingProblems.throwErrorOnWarning = false; class ParsingError { constructor(message, meta) { this.message = message; this.meta = meta; this.name = 'OAS3 Parsing Error'; } } /* tslint:disable triple-equals */ const defaultConfig = { jsonPathRegex: /([\w:\/\\\.]+)?#(\/?[\w+\/?]+)/, implicitTypesRefReplacement: false, parametersModelName: (baseTypeName) => `${baseTypeName}Parameters`, headersModelName: (baseTypeName, code) => `${baseTypeName}HeadersResponse${code}`, requestModelName: (baseTypeName) => `${baseTypeName}Request`, responseModelName: (baseTypeName, code, contentTypeKey = null) => `${baseTypeName}${___default.capitalize(contentTypeKey)}Response${code}`, typingsDirectory: './typings', mocksDirectory: './mocks', excludeFromComparison: [ 'description', 'title', 'example', 'default', 'readonly', ], defaultServerInfo: [ { url: 'http://localhost' } ] }; const jsonPathRegex = /([\w:\/\\\.]+)?#(\/?[\w+\/?]+)/; /** * @param pieces * @return * JSON-Path according to [RFC-6901](https://tools.ietf.org/html/rfc6901) * TODO refactor: move to common helper */ function makeJsonPath(...pieces) { return jsonPointer.compile(pieces); } /** * Rethrow {@link ParsingError} if original error already handled at inner level * @param {ParsingError | any} error */ function rethrowParsingError(error) { if (error instanceof ParsingError) { throw error; } } /** * Базовый класс загрузчика. */ class BaseConvertor { constructor(config = defaultConfig) { this.config = config; } loadOAPI3Structure(structure) { if (!structure || !___default.isObjectLike(structure)) { throw new ParsingError([ `Expected structure to be object but got:`, JSON.stringify(structure, null, ' ') ].join('\n')); } this._operationsMeta = []; this._structure = structure; } setForeignSchemeFn(fn) { if (!___default.isFunction(fn)) { throw new ParsingError('Error in `setForeignSchemeFn`: argument has to be function!'); } this._foreignSchemaFn = (jsonPath) => { try { return fn(jsonPath); } catch (e) { throw new ParsingError('Error when trying to resolve schema!', { jsonPath }); } }; } getApiMeta() { return this._operationsMeta; } /** * Getting of "entry-points" from structure in {@link _structure} early * loaded by {@link loadOAPI3Structure}. * * Entrypoints are: * * - Parameters * - Requests * - Responses * * ### Why it needed? * * Entrypoints is needed to use [Convertor.renderRecursive]{@link Convertor.renderRecursive}. * It's like a pulling on thread where entrypoints are outstanding trheads. * * @param ApiMetaInfo metaInfo * Mutable object where meta-information accumulates during * API-info extracting. */ getOAPI3EntryPoints(context = {}, metaInfo = []) { const alreadyConverted = []; // parameters const methodsSchemes = this.getMethodsSchemes(metaInfo); const dataTypeContainers = ___default.map(methodsSchemes, (schema, modelName) => { const container = this.convert(schema, context, modelName); // TODO Crutch. This class should never know about assignedTypes // in GenericDescriptor if (schema instanceof Generic) { ___default.each(container, (description) => { if (description['assignedTypes']) { description['assignedTypes'] = ['TCode', 'TContentType', 'T1', 'T2']; } }); } // Исключение дубликатов. // Дубликаты появляются, когда типы данные, которые // ссылаются ($ref) без изменения на другие, подменяются // моделями из `schema`. return ___default.map(container, (descr) => { // Excluding of elements having common `originalSchemaPath` // and simultaneously already related with. if (descr.originalSchemaPath) { if (___default.findIndex(alreadyConverted, v => v === descr.originalSchemaPath) !== -1) { return null; } alreadyConverted.push(descr.originalSchemaPath); } return descr; }); }); const dataTypeContainer = ___default.flattenDepth(dataTypeContainers); return ___default.compact(dataTypeContainer); } /** * Получить дескриптор типа по JSON Path: * возвращает уже созданный ранее, или создает * новый при первом упоминании. * * @param path * @param context */ findTypeByPath(path, context) { const alreadyFound = ___default.find(___default.values(context), (v) => v.originalSchemaPath === path); return alreadyFound ? [alreadyFound] : this._processSchema(path, context); } /** * @deprecated will be renamed to `getSchemaByRef` * @param ref * @param pathWhereReferred * @return */ getSchemaByPath(ref, pathWhereReferred) { const pathMatches = ref.match(jsonPathRegex); if (pathMatches) { const filePath = pathMatches[1]; const schemaPath = pathMatches[2]; const src = filePath ? this._getForeignSchema(filePath) : this._structure; const result = ___default.get(src, ___default.trim(schemaPath, '#/\\').replace(/[\\\/]/g, '.'), undefined); if (result === undefined) { ParsingProblems.parsingWarning(`Cant resolve ${ref}!`, { oasStructure: this._structure, relatedRef: ref, jsonPath: pathWhereReferred ? makeJsonPath(...pathWhereReferred) : undefined }); } return result; } else { throw new ParsingError(`JSON Path error: ${ref} is not valid JSON path!`, { oasStructure: this._structure, relatedRef: ref, jsonPath: pathWhereReferred ? makeJsonPath(...pathWhereReferred) : undefined }); } } /** * Извлечени схем из параметров, ответов и тел запросов для API. * @param metaInfo * Place for storing meta-info of API-method. */ getMethodsSchemes(metaInfo) { const struct = this._structure; if (!struct) { throw new ParsingError([ 'There is no structure loaded!', 'Please, call method `loadOAPI3Structure` before!' ].join(' ')); } const result = {}; const paths = struct.paths; if (!paths) { throw new ParsingError('No paths presented in OAS structure!', { oasStructure: struct }); } for (const path in paths) { // skip proto's properties if (!paths.hasOwnProperty(path)) { continue; } const jsonPathToPath = ['paths', path]; if (!path) { ParsingProblems.parsingWarning('Path key cant be empty. Skipped.', { oasStructure: struct, jsonPath: makeJsonPath(...jsonPathToPath) }); continue; } const pathItem = struct.paths[path]; if (!___default.isObjectLike(pathItem)) { ParsingProblems.parsingWarning('Item of "paths" should be object like. Skipped.', { oasStructure: struct, jsonPath: makeJsonPath(...jsonPathToPath) }); continue; } const methods = [ 'delete', 'get', 'head', 'options', 'patch', 'post', 'put', 'trace', ]; for (const methodName of methods) { // skip proto's properties if (!pathItem.hasOwnProperty(methodName)) { continue; } const apiOperation = pathItem[methodName]; const jsonPathToOperation = [...jsonPathToPath, methodName]; if (!___default.isObjectLike(apiOperation)) { ParsingProblems.parsingWarning('Operation should be object like. Skipped.', { oasStructure: struct, jsonPath: makeJsonPath(...jsonPathToOperation) }); continue; } const baseTypeName = this._getOperationBaseName(apiOperation, methodName, path, jsonPathToOperation); const servers = this._getOperationServers(apiOperation, jsonPathToOperation); const metaInfoItem = { apiSchemaFile: 'domain-api-schema', baseTypeName, headersSchema: null, headersModelName: null, method: methodName.toUpperCase(), mockData: {}, paramsModelName: null, paramsSchema: null, path, queryParams: [], requestIsRequired: false, requestModelName: null, requestSchema: null, responseModelName: null, responseSchema: null, servers, // default noname typingsDependencies: [], typingsDirectory: 'typings', }; let jsonSubPathToOperation; try { jsonSubPathToOperation = [...jsonPathToOperation, 'parameters']; // pick Parameters schema ___default.assign(result, this._pickApiMethodParameters(metaInfoItem, apiOperation.parameters, jsonSubPathToOperation)); } catch (error) { rethrowParsingError(error); throw new ParsingError('An error occurred at method parameters fetching', { oasStructure: struct, jsonPath: makeJsonPath(...jsonSubPathToOperation), originalError: error }); } try { jsonSubPathToOperation = [...jsonPathToOperation, 'responses']; // pick Responses schema ___default.assign(result, this._pickApiMethodResponses(metaInfoItem, apiOperation.responses || {}, jsonSubPathToOperation)); } catch (error) { rethrowParsingError(error); throw new ParsingError('An error occurred at method responses fetching', { oasStructure: struct, jsonPath: makeJsonPath(...jsonSubPathToOperation), originalError: error }); } try { jsonSubPathToOperation = [...jsonPathToOperation, 'requestBody']; // pick Request Body schema ___default.assign(result, this._pickApiMethodRequest(apiOperation, metaInfoItem, [...jsonSubPathToOperation])); } catch (error) { rethrowParsingError(error); throw new ParsingError('An error occurred at method request body fetching', { oasStructure: struct, jsonPath: makeJsonPath(...jsonSubPathToOperation), originalError: error }); } metaInfo.push(metaInfoItem); this._operationsMeta.push({ operationJsonPath: jsonPathToOperation, apiMeta: metaInfoItem, }); } } return result; } /** * Get parameters from the `parameters` section * in method into {@link ApiMetaInfo}-object */ _pickApiMethodParameters(metaInfoItem, parameters, jsonPathToOperation) { const result = {}; const paramsModelName = this.config.parametersModelName(metaInfoItem.baseTypeName); const paramsSchema = { properties: {}, required: [], type: 'object' }; // process parameters ___default.each(parameters || [], (parameter, index) => { if (parameter.$ref) { parameter = ___default.merge(___default.omit(parameter, ['$ref']), this.getSchemaByPath(parameter.$ref, [...jsonPathToOperation, String(index)])); } if (parameter.schema) { paramsSchema.properties[parameter.name] = parameter.schema; if (parameter.description) { parameter.schema.description = parameter.description; } if (parameter.readOnly) { paramsSchema.properties[parameter.name].readOnly = true; } if (parameter.required) { paramsSchema.required.push(parameter.name); } if (Number(index) === 0) { // metaInfoItem.typingsDependencies.push(paramsModelName); metaInfoItem.paramsModelName = paramsModelName; metaInfoItem.paramsSchema = paramsSchema; } if (parameter.in === Oas3ParameterTarget.Query) { metaInfoItem.queryParams.push(parameter.name); } if (!paramsSchema.description) { paramsSchema.description = `Model of parameters for API \`${metaInfoItem.path}\``; } result[paramsModelName] = paramsSchema; } }); if (result[paramsModelName]) { metaInfoItem.typingsDependencies.push(paramsModelName); } return result; } _pickApiMethodResponses(metaInfoItem, responses, jsonPathToOperation) { const result = {}; ___default.each(responses, (response, code) => { // todo do tests when $ref to schema.responses if (response.$ref) { response = ___default.merge(___default.omit(response, ['$ref']), this.getSchemaByPath(response.$ref, [...jsonPathToOperation, String(code)])); } const mediaTypes = response.content || response.schema // Case for OAS2 || {}; // Fallback // if content set, but empty if (___default.keys(mediaTypes).length === 0) { mediaTypes['application/json'] = null; } // todo пока обрабатываются только контент и заголовки ___default.each(mediaTypes || {}, (mediaContent, contentTypeKey) => { // todo do fallback if no `schema property` const schema = ___default.get(mediaContent, 'schema') || { description: 'Empty response', type: 'null', }; // by default if (!metaInfoItem.responseSchema) { metaInfoItem.responseSchema = {}; } // add description if it's set if (response.description) { schema.description = response.description; } ___default.set(metaInfoItem.responseSchema, [code, contentTypeKey], schema); }); if (response.headers) { // TODO has to be tested const modelName = this.config.headersModelName(metaInfoItem.baseTypeName, code); result[modelName] = { properties: response.headers, type: 'object' }; } }); if (metaInfoItem.responseSchema) { const modelName = this.config.responseModelName(metaInfoItem.baseTypeName, '', ''); metaInfoItem.responseModelName = modelName; metaInfoItem.typingsDependencies.push(modelName); result[modelName] = new Generic(___default.mapValues(metaInfoItem.responseSchema, subSchema => new Generic(subSchema))); } return result; } _pickApiMethodRequest(apiOperation, metaInfoItem, jsonPathToOperation) { const { requestBody } = apiOperation; let responses; if (!requestBody) { return null; } else { metaInfoItem.requestIsRequired = !!requestBody.required; if (!requestBody.content) { return null; } const modelName = this.config.requestModelName(metaInfoItem.baseTypeName); responses = ___default(requestBody.content) .mapValues((mediaContent, mediaTypeName) => { const mapResult = mediaContent.schema ? Object.assign({}, mediaContent.schema) : null; if (apiOperation.requestBody.description && !mapResult.description) { mapResult.description = apiOperation.requestBody.description; } return mapResult; }) .value(); metaInfoItem.requestModelName = modelName; metaInfoItem.requestSchema = responses; metaInfoItem.typingsDependencies.push(modelName); return ___default.zipObject([modelName], [new Generic(responses)]); } } /** * Получение нового дескриптора на основе JSON Path * из текущей структуры. * * @param path * @param context */ _processSchema(path, context) { const schema = this.getSchemaByPath(path); const modelName = (___default.trim(path, '/\\').match(/(\w+)$/) || [])[1]; if (!schema) { throw new Error(`Error: can't find schema with path: ${path}!`); } const results = this.convert(schema, context, modelName, null, path); ___default.each(results, (result) => { context[result.originalSchemaPath || result.modelName] = result; }); return results; } _getForeignSchema(ref) { if (this._foreignSchemaFn) { return this._foreignSchemaFn(ref); } else { throw new ParsingError([ 'Function for getting foreign scheme not set.', `Use setForeignSchemeFn(). Path: ${ref}.` ].join('\n'), { oasStructure: this._structure, relatedRef: ref }); } } _getOperationBaseName(apiOperation, methodName, path, jsonPathToOperation) { if (!apiOperation.operationId || !___default.isString(apiOperation.operationId)) { const baseNameFallback = ___default.upperFirst(___default.camelCase(jsonPathToOperation.join('-').replace(/[^\-\w]/g, ''))); ParsingProblems.parsingWarning([ `Wrong operation id "${apiOperation.operationId}".`, `Fallback basename is: "${baseNameFallback}".` ].join('\n'), { oasStructure: this._structure, jsonPath: makeJsonPath(...jsonPathToOperation, 'operationId') }); return baseNameFallback; } const operationId = ___default.camelCase(apiOperation.operationId.trim().replace(/[^\w+]/g, '_')); return ___default.upperFirst(operationId) || [ ___default.capitalize(methodName), ___default.upperFirst(___default.camelCase(path)) ].join(''); } _getOperationServers(apiOperation, jsonPathToOperation) { let servers = apiOperation.servers || this._structure.servers; if (servers !== undefined && !___default.isArray(servers)) { ParsingProblems.parsingWarning('Servers should be array. Skipped.', { oasStructure: this._structure, jsonPath: makeJsonPath(...jsonPathToOperation, 'servers') }); servers = []; } if (!servers || servers.length < 1) { servers = defaultConfig.defaultServerInfo; } return servers; } } const commonPrettierOptions = { plugins: [prettierParserMD, prettierParserTS], proseWrap: 'always', singleQuote: true }; class AbstractTypeScriptDescriptor { constructor(schema, /** * Родительский конвертор, который используется * чтобы создавать вложенные дескрипторы. */ convertor, /** * Рабочий контекст */ context, /** * Название этой модели (может быть string * или null). */ modelName, /* * Предлагаемое имя для типа данных: может * применяться, если тип данных анонимный, но * необходимо вынести его за пределы родительской * модели по-ситуации (например, в случае с Enum). */ suggestedModelName, /** * Путь до оригинальной схемы, на основе * которой было создано описание этого типа данных. */ originalSchemaPath, /** * Родительсткие модели. */ ancestors) { this.schema = schema; this.convertor = convertor; this.context = context; this.modelName = modelName; this.suggestedModelName = suggestedModelName; this.originalSchemaPath = originalSchemaPath; this.ancestors = ancestors; } /** * todo todo https://github.com/koshevy/codegena/issues/33 */ getComments() { return this.makeComment(this.schema.title, this.schema.description); } toString() { return `${this.modelName || 'Anonymous Type'}${this.originalSchemaPath ? `(${this.originalSchemaPath})` : ''}`; } /** * Formatting code. Supports TypeScript and Markdown essences. * todo https://github.com/koshevy/codegena/issues/33 * * @param code * @param codeType */ formatCode(code, codeType = 'typescript') { const prettierOptions = Object.assign({ parser: codeType }, commonPrettierOptions); return prettier.format(code, prettierOptions); } /** * todo todo https://github.com/koshevy/codegena/issues/33 */ makeComment(title, description) { const markdownText = this.formatCode([ title ? `## ${title}` : '', (description || '').trim() ].join('\n'), 'markdown'); const commentLines = ___default.compact(markdownText.split('\n')); let comment = ''; if (commentLines.length) { comment = `/**\n${___default.map(commentLines, v => ` * ${v}`).join('\n')}\n */\n`; } return comment; } } class AnyTypeScriptDescriptor extends AbstractTypeScriptDescriptor { constructor(schema, /** * Родительский конвертор, который используется * чтобы создавать вложенные дескрипторы. */ convertor, /** * Рабочий контекст */ context, /** * Название этой модели (может быть string * или null). */ modelName, /* * Предлагаемое имя для типа данных: может * применяться, если тип данных анонимный, но * необходимо вынести его за пределы родительской * модели по-ситуации (например, в случае с Enum). */ suggestedModelName, /** * Путь до оригинальной схемы, на основе * которой было создано описание этого типа данных. */ originalSchemaPath) { super(schema, convertor, context, modelName, suggestedModelName, originalSchemaPath); this.schema = schema; this.context = context; this.modelName = modelName; this.suggestedModelName = suggestedModelName; this.originalSchemaPath = originalSchemaPath; } /** * Рендер типа данных в строку. * * @param childrenDependencies * Immutable-массив, в который складываются все зависимости * типов-потомков (если такие есть). * @param rootLevel * Говорит о том, что это рендер "корневого" * уровня — то есть, не в составе другого типа, * а самостоятельно. * */ render(childrenDependencies, rootLevel = true) { const comment = this.getComments(); return `${rootLevel ? `${comment}export type ${this.modelName} = ` : ''}any`; } } class ArrayTypeScriptDescriptor extends AbstractTypeScriptDescriptor { constructor(schema, /** * Родительский конвертор, который используется * чтобы создавать вложенные дескрипторы. */ convertor, /** * Рабочий контекст */ context, /** * Название этой модели (может быть string * или null). */ modelName, /* * Предлагаемое имя для типа данных: может * применяться, если тип данных анонимный, но * необходимо вынести его за пределы родительской * модели по-ситуации (например, в случае с Enum). */ suggestedModelName, /** * Путь до оригинальной схемы, на основе * которой было создано описание этого типа данных. */ originalSchemaPath) { super(schema, convertor, context, modelName, suggestedModelName, originalSchemaPath); this.schema = schema; this.convertor = convertor; this.context = context; this.modelName = modelName; this.suggestedModelName = suggestedModelName; this.originalSchemaPath = originalSchemaPath; const parentName = (modelName || suggestedModelName); if (schema.items && ___default.isArray(schema.items)) { this.exactlyListedItems = ___default.map(schema.items, (schemaItem, index) => convertor.convert(schemaItem, context, null, parentName ? `${parentName}Item${index}` : null)); } else if (schema.items) { this.commonItemType = convertor.convert(schema.items, context, null, parentName ? `${parentName}Items` : null); } } /** * Рендер типа данных в строку. * * @param childrenDependencies * Immutable-массив, в который складываются все зависимости * типов-потомков (если такие есть). * @param rootLevel * Говорит о том, что это рендер "корневого" * уровня — то есть, не в составе другого типа, * а самостоятельно. * */ render(childrenDependencies, rootLevel = true) { const comment = this.getComments(); const result = `${rootLevel ? `${comment}export type ${this.modelName} = ` : ''}${this._renderArrayItemsTypes(childrenDependencies)}`; return rootLevel ? this.formatCode(result) : result; } _renderArrayItemsTypes(childrenDependencies) { if (this.exactlyListedItems) { return `[\n${___default.map(this.exactlyListedItems, (itemDescrs) => { return ___default.map(itemDescrs, (descr) => descr.render(childrenDependencies, false) + (descr.schema['description'] ? `, // ${descr.schema['description']}` : ',')); }).join('\n')}\n]`; } else if (this.commonItemType) { return ___default.map(this.commonItemType, (descr) => `Array<${descr.render(childrenDependencies, false)}>`).join(' | '); } else { return 'any[]'; } } } class BooleanTypeScriptDescriptor extends AbstractTypeScriptDescriptor { constructor(schema, /** * Родительский конвертор, который используется * чтобы создавать вложенные дескрипторы. */ convertor, /** * Рабочий контекст */ context, /** * Название этой модели (может быть string * или null). */ modelName, /* * Предлагаемое имя для типа данных: может * применяться, если тип данных анонимный, но * необходимо вынести его за пределы родительской * модели по-ситуации (например, в случае с Enum). */ suggestedModelName, /** * Путь до оригинальной схемы, на основе * которой было создано описание этого типа данных. */ originalSchemaPath) { super(schema, convertor, context, modelName, suggestedModelName, originalSchemaPath); this.schema = schema; this.context = context; this.modelName = modelName; this.suggestedModelName = suggestedModelName; this.originalSchemaPath = originalSchemaPath; } /** * Рендер типа данных в строку. * * @param childrenDependencies * Immutable-массив, в который складываются все зависимости * типов-потомков (если такие есть). * @param rootLevel * Говорит о том, что это рендер "корневого" * уровня — то есть, не в составе другого типа, * а самостоятельно. * */ render(childrenDependencies, rootLevel = true) { const comment = this.getComments(); return `${rootLevel ? `${comment}export type ${this.modelName} = ` : ''}boolean`; } } class EnumTypeScriptDescriptor extends AbstractTypeScriptDescriptor { constructor(schema, convertor, context, modelName, suggestedModelName, originalSchemaPath) { super(schema, convertor, context, modelName || EnumTypeScriptDescriptor.getNewEnumName(suggestedModelName), suggestedModelName, originalSchemaPath); this.schema = schema; this.convertor = convertor; this.context = context; this.modelName = modelName; this.suggestedModelName = suggestedModelName; this.originalSchemaPath = originalSchemaPath; this.propertiesSets = [{}]; } static getNewEnumName(suggestedModelName) { const name = suggestedModelName ? `${suggestedModelName}Enum` : `Enum`; if (!this._usedNames[name]) { this._usedNames[name] = 1; } else { this._usedNames[name]++; } return `${name}${(this._usedNames[name] > 1) ? `_${this._usedNames[name] - 2}` : ''}`; } render(childrenDependencies, rootLevel = true) { const { modelName } = this; if (!modelName) { return this.renderInlineTypeVariant(); } if (!rootLevel && modelName) { childrenDependencies.push(this); return modelName; } // root level switch (this.schema.type) { case 'string': return this.renderRootStringEnum(); default: return this.renderInlineTypeVariant(true); } } renderInlineTypeVariant(rootLevel = false) { const inline = ___default.map(this.schema.enum, enumItem => JSON.stringify(enumItem)) .join(' | '); return rootLevel ? `${this.getComments()} export type ${this.modelName} = ${inline}` : inline; } renderRootStringEnum() { const humaniedVariableNames = ___default(this.schema.enum) .map(___default.camelCase) .map(enumItem => /^[a-z]/.test(enumItem) ? enumItem : `_${enumItem}`) .map(___default.upperFirst) .uniq() .value(); if (humaniedVariableNames.length === this.schema.enum.length) { return `export enum ${this.modelName} {${___default.map(humaniedVariableNames, (varName, index) => `${varName} = ${JSON.stringify(this.schema.enum[index])}`)}}`; } else { return this.renderInlineTypeVariant(true); } } } EnumTypeScriptDescriptor._usedNames = {}; class GenericDescriptor extends AbstractTypeScriptDescriptor { constructor(schema, /** * Родительский конвертор, который используется * чтобы создавать вложенные дескрипторы. */ convertor, /** * Рабочий контекст */ context, /** * Название этой модели (может быть string * или null). */ modelName, /* * Предлагаемое имя для типа данных: может * применяться, если тип данных анонимный, но * необходимо вынести его за пределы родительской * модели по-ситуации (например, в случае с Enum). */ suggestedModelName, /** * Путь до оригинальной схемы, на основе * которой было создано описание этого типа данных. */ originalSchemaPath) { super(schema, convertor, context, modelName, suggestedModelName, originalSchemaPath); this.schema = schema; this.convertor = convertor; this.context = context; this.modelName = modelName; this.suggestedModelName = suggestedModelName; this.originalSchemaPath = originalSchemaPath; this.assignedTypes = ['T1', 'T2', 'T3', 'T4', 'T5', 'T6', 'T7']; this.children = ___default.mapValues(schema.children, (child, key) => convertor.convert(child, context, null, this._getChildSuggestedName(key))); } render(childrenDependencies, rootLevel = true) { const comment = this.getComments(); const rangeOfValues = this.getDeepRangeOfValuesMatrix(); const rangeOfValuesTypes = ___default.map(rangeOfValues, (valuesOnLevel, level) => { const valueRange = ___default.map(valuesOnLevel, value => JSON.stringify(Number(value) || value)).join(' | '); return `${this.assignedTypes[level]} extends ${valueRange} = ${valueRange}`; }).join(', '); const result = `${rootLevel ? `${comment}export type ${this.modelName}<${rangeOfValuesTypes}> = ` : ''}${this.subRender(childrenDependencies, this.assignedTypes)};`; return rootLevel ? this.formatCode(result) : result; } subRender(childrenDependencies, assignedTypeAliases) { const [assignedToKey] = assignedTypeAliases; let result = ''; ___default.mapValues(this.children, (childGroup, associatedValue) => { const [child] = childGroup; result += `${assignedToKey} extends ${JSON.stringify(Number(associatedValue) || associatedValue)}`; if (child instanceof GenericDescriptor) { result += ' ? '; result += child.subRender(childrenDependencies, assignedTypeAliases.slice(1)); } else { result += ' ? '; result += ___default.map(childGroup, (simpleChild) => { return simpleChild.getComments() + '\n' + simpleChild.render(childrenDependencies, false); }).join('\n | '); } result += ' : '; }); result += 'any'; return result; } toString() { return `${this.modelName || 'Anonymous Generic Type'}${this.originalSchemaPath ? `(${this.originalSchemaPath})` : ''}`; } _getChildSuggestedName(key) { const postfix = ___default.upperFirst(key.replace(/[^\w]+/g, '_')); return `${this.modelName}`; } getDeepRangeOfValuesMatrix() { const result = []; function collectMatrixLevel(descriptior, level, matrix) { if (!matrix[level]) { matrix.push([]); } ___default.each(descriptior.children, (column, keyInColumn) => { matrix[level].push(keyInColumn); ___default.each(column, (descriptionInColumn) => { if (descriptionInColumn instanceof GenericDescriptor) { collectMatrixLevel(descriptionInColumn, level + 1, matrix); } }); }); } collectMatrixLevel(this, 0, result); return ___default.map(result, (valuesOnLevel) => ___default.uniq(valuesOnLevel)); } } class InstanceofDescriptior extends AbstractTypeScriptDescriptor { constructor(schema, /** * Родительский конвертор, который используется * чтобы создавать вложенные дескрипторы. */ convertor, /** * Рабочий контекст */ context, /** * Название этой модели (может быть string * или null). */ modelName, /* * Предлагаемое имя для типа данных: может * применяться, если тип данных анонимный, но * необходимо вынести его за пределы родительской * модели по-ситуации (например, в случае с Enum). */ suggestedModelName, /** * Путь до оригинальной схемы, на основе * которой было создано описание этого типа данных. */ originalSchemaPath) { super(schema, convertor, context, modelName, suggestedModelName, originalSchemaPath); this.schema = schema; this.convertor = convertor; this.context = context; this.modelName = modelName; this.suggestedModelName = suggestedModelName; this.originalSchemaPath = originalSchemaPath; this.instanceOf = schema['instanceof']; if (schema['x-generic']) { this.genericOf = convertor.convert(schema['x-generic'], context, null, (modelName || suggestedModelName) ? `${(modelName || suggestedModelName)}FormDataFormat` : null); } } /** * Рендер типа данных в строку. * * @param childrenDependencies * Immutable-массив, в который складываются все зависимости * типов-потомков (если такие есть). * @param rootLevel * Говорит о том, что это рендер "корневого" * уровня — то есть, не в составе другого типа, * а самостоятельно. * */ render(childrenDependencies, rootLevel = true) { const comment = this.getComments(); return `${rootLevel ? `${comment}export type ${this.modelName} ${this.genericOf ? `<${this.genericOf}>` : ''} = ` : ''}${this.instanceOf}`; } } class NullTypeScriptDescriptor extends AbstractTypeScriptDescriptor { constructor(schema, /** * Родительский конвертор, который используется * чтобы создавать вложенные дескрипторы. */ convertor, /** * Рабочий контекст */ context, /** * Название этой модели (может быть string * или null). */ modelName, /* * Предлагаемое имя для типа данных: может * применяться, если тип данных анонимный, но * необходимо вынести его за пределы родительской * модели по-ситуации (например, в случае с Enum). */ suggestedModelName, /** * Путь до оригинальной схемы, на основе * которой было создано описание этого типа данных. */ originalSchemaPath) { super(schema, convertor, context, modelName, suggestedModelName, originalSchemaPath); this.schema = schema; this.context = context; this.modelName = modelName; this.suggestedModelName = suggestedModelName; this.originalSchemaPath = originalSchemaPath; } /** * Рендер типа данных в строку. * * @param childrenDependencies * Immutable-массив, в который складываются все зависимости * типов-потомков (если такие есть). * @param rootLevel * Говорит о том, что это рендер "корневого" * уровня — то есть, не в составе другого типа, * а самостоятельно. * */ render(childrenDependencies, rootLevel = true) { const comment = this.getComments(); return `${rootLevel ? `${comment}export type ${this.modelName} = ` : ''}null`; } } class NumberTypeScriptDescriptor extends AbstractTypeScriptDescriptor { constructor(schema, /** * Родительский конвертор, который используется * чтобы создавать вложенные дескрипторы. */ convertor, /** * Рабочий контекст */ context, /** * Название этой модели (может быть string * или null). */ modelName, /* * Предлагаемое имя для типа данных: может * применяться, если тип данных анонимный, но * необходимо вынести его за пределы родительской * модели по-ситуации (например, в случае с Enum). */ suggestedModelName, /** * Путь до оригинальной схемы, на основе * которой было создано описание этого типа данных. */ originalSchemaPath) { super(schema, convertor, context, modelName, suggestedModelName, originalSchemaPath); this.schema = schema; this.context = context; this.modelName = modelName; this.suggestedModelName = suggestedModelName; this.originalSchemaPath = originalSchemaPath; } /** * Рендер типа данных в строку. * * @param childrenDependencies * Immutable-массив, в который складываются все зависимости * типов-потомков (если такие есть). * @param rootLevel * Говорит о том, что это рендер "корневого" * уровня — то есть, не в составе другого типа, * а самостоятельно. * */ render(childrenDependencies, rootLevel = true) { const comment = this.getComments(); if (rootLevel && !(this.modelName || this.suggestedModelName)) { throw new Error('Type can\'t be rendered as root! Should have `modelName` or `suggestedModelName`'); } return `${rootLevel ? `${comment}export type ${this.modelName || this.suggestedModelName} = ` : ''}number`; } } class ObjectTypeScriptDescriptor extends AbstractTypeScriptDescriptor { constructor(schema, convertor, context, modelName, suggestedModelName, originalSchemaPath, ancestors) { super(schema, convertor, context, modelName, suggestedModelName, originalSchemaPath, ancestors); this.schema = schema; this.convertor = convertor; this.context = context; this.modelName = modelName; this.suggestedModelName = suggestedModelName; this.originalSchemaPath = originalSchemaPath; this.ancestors = ancestors; /** * Свойства, относящиеся к этому объекту * (интерфейсы и классы). */ this.propertiesSets = [{}]; // autofill properties from `required` not presented in `properties` // with default options ___default.each(schema.required || [], propertyName => { if (!schema.properties) { schema.properties = {}; } if (!schema.properties[propertyName]) { schema.properties[propertyName] = { description: "Auto filled property from `required`" }; } }); if (schema.properties) { ___default.each(schema.properties, (propSchema, propName) => { const suggestedName = (modelName || suggestedModelName || '') + ___default.camelCase(propName).replace(/^./, propName[0].toUpperCase()); const typeContainer = convertor.convert(this._applyNullableInProp(propSchema), context, null, suggestedName); let comment; const isReadyDescriptor = !___default.isEmpty(typeContainer) && !___default.isEmpty(typeContainer[0]); if (propSchema.title || propSchema.description) { comment = this.makeComment(propSchema.title, propSchema.description); } else { comment = isReadyDescriptor ? typeContainer[0].getComments() : ''; } this.propertiesSets[0][propName] = { required: ___default.findIndex(schema.required || [], v => v === propName) !== -1, readOnly: propSchema.readOnly || propSchema['readonly'], typeContainer, comment, defaultValue: propSchema.default, exampleValue: isReadyDescriptor ? this._findExampleInTypeContainer(typeContainer) : undefined }; }); } // обработка свойств предков if (this.ancestors) { ___default.each(this.ancestors, ancestor => { const ancestorProperties = ___default.mapValues(ancestor['propertiesSets'][0] || {}, // applying local `required` for inherited props (property, propertyName) => { return ___default.includes(this.schema.required || [], propertyName) ? Object.assign(Object.assign({}, property), { required: true }) : property; }); ___default.assign(this.propertiesSets[0], ancestorProperties); }); } else if ( // если по итогам, свойств нет, указывается // универсальное описание schema.additionalProperties || (!___default.keys(this.propertiesSets[0] || {}).length && (schema.additionalProperties !== false))) { const addProp = schema.additionalProperties; const typeContainer = ('object' === typeof addProp) ? convertor.convert( // these properties does not affect a schema this._applyNullableInProp(___default.omit(addProp, defaultConfig.excludeFromComparison)), context, null, `${modelName}Properties`) : convertor.convert({}, {}); this.propertiesSets[0]['[key: string]'] = { comment: typeContainer[0] ? typeContainer[0].getComments() : '', defaultValue: undefined, exampleValue: undefined, readOnly: ('object' === typeof addProp) ? addProp.readOnly || false : false, required: true, // если нет свойств, получает тип Any typeContainer }; } } /** * Рендер типа данных в строку. * * @param childrenDependencies * Immutable-массив, в который складываются все зависимости * типов-потомков (если такие есть). * @param rootLevel * Говорит о том, что это рендер "корневого" * уровня — то есть, не в составе другого типа, * а самостоятельно. * */ render(childrenDependencies, rootLevel = true) { if (rootLevel && !this.modelName) { throw new Error('Root object models should have model name!'); } else if (!rootLevel && this.modelName) { childrenDependencies.push(this); // если это не rootLevel, и есть имя, // то просто выводится имя return this.modelName; } const comment = this.getComments(); const prefix = (rootLevel) ? (this.propertiesSets.length > 1 ? `${comment}export type ${this.modelName} = ` : `${comment}export interface ${this.modelName}${this._renderExtends(childrenDependencies)}`) : ''; // рекурсивно просчитывает вложенные свойства const properties = ___default.map(this.propertiesSets, (propertySet) => `{ ${___default.values(___default.map(propertySet, (descr, name) => { const propName = name.match(/\-/) ? `'${name}'` : name; return `\n${descr.comment}${descr.readOnly ? 'readonly ' : ''}${propName}${!descr.required ? '?' : ''}: ${___default.map(descr.typeContainer, type => type.render(childrenDependencies, false)).join('; ')}`; })).join('; ')} }`).join(' | '); if (rootLevel) { return this.formatCode([prefix, properties].join('')); } else { return [prefix, properties].join(''); } } getExampleValue() { return this.schema.example || ___default.mapValues(this.propertiesSets[0], (v) => v.exampleValue || v.defaultValue); } /** * Превращение "ancestors" в строку. */ _renderExtends(dependencies) { let filteredAncestors = []; if (this.ancestors && this.ancestors.length) { filteredAncestors = ___default.filter(this.ancestors, ancestor => !!ancestor.modelName); } dependencies.push.apply(dependencies, filteredAncestors); return filteredAncestors.length ? ` extends ${___default.map(filteredAncestors, v => v.modelName).join(', ')} ` : ''; } _findExampleInTypeContainer(typeContainer) { for (const descr of typeContainer) { if (descr instanceof ObjectTypeScriptDescriptor) { return descr.getExampleValue(); }