@codegena/oapi3ts
Version:
Codegeneration from OAS3 to TypeScript
1,302 lines (1,288 loc) • 92.5 kB
JavaScript
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();
}