- v3 generator

This commit is contained in:
Ferdi Koomen 2019-11-23 13:47:27 +01:00
parent cd8b72b080
commit b92a02a907
38 changed files with 3749 additions and 2110 deletions

View File

@ -1,7 +1,7 @@
import { Model } from './Model';
export interface OperationParameter extends Model {
in: 'path' | 'query' | 'header' | 'formData' | 'body';
in: 'path' | 'query' | 'header' | 'formData' | 'body' | 'cookie';
prop: string;
default?: any;
}

View File

@ -0,0 +1,44 @@
export enum PrimaryType {
FILE = 'File',
OBJECT = 'any',
BOOLEAN = 'boolean',
NUMBER = 'number',
STRING = 'string',
VOID = 'void',
NULL = 'null',
}
export const TYPE_MAPPINGS = new Map<string, PrimaryType>([
['file', PrimaryType.FILE],
['binary', PrimaryType.FILE],
['any', PrimaryType.OBJECT],
['object', PrimaryType.OBJECT],
['boolean', PrimaryType.BOOLEAN],
['byte', PrimaryType.NUMBER],
['int', PrimaryType.NUMBER],
['int32', PrimaryType.NUMBER],
['int64', PrimaryType.NUMBER],
['integer', PrimaryType.NUMBER],
['float', PrimaryType.NUMBER],
['double', PrimaryType.NUMBER],
['short', PrimaryType.NUMBER],
['long', PrimaryType.NUMBER],
['number', PrimaryType.NUMBER],
['char', PrimaryType.STRING],
['date', PrimaryType.STRING],
['date-time', PrimaryType.STRING],
['password', PrimaryType.STRING],
['string', PrimaryType.STRING],
['void', PrimaryType.VOID],
['null', PrimaryType.NULL],
]);
export enum Method {
GET = 'get',
PUT = 'put',
POST = 'post',
DELETE = 'delete',
OPTIONS = 'options',
HEAD = 'head',
PATCH = 'patch',
}

View File

@ -0,0 +1,13 @@
import { EOL } from 'os';
export function getComment(comment?: string): string | null {
if (comment) {
return comment
.split(/(\r\n|\n|\r)+/g)
.filter(line => line)
.map(line => line.trim())
.join(EOL)
.replace(/(\r\n|\n|\r)+/g, '$1 * ');
}
return null;
}

View File

@ -0,0 +1,26 @@
import { Enum } from '../../../client/interfaces/Enum';
import { PrimaryType } from './constants';
export function getEnum(values?: (string | number)[]): Enum[] {
if (Array.isArray(values)) {
return values
.filter((value, index, arr) => {
return arr.indexOf(value) === index;
})
.map(value => {
if (typeof value === 'number') {
return {
name: `NUM_${value}`,
value: String(value),
type: PrimaryType.NUMBER,
};
}
return {
name: value.replace(/([a-z])([A-Z]+)/g, '$1_$2').toUpperCase(),
value: `'${value}'`,
type: PrimaryType.STRING,
};
});
}
return [];
}

View File

@ -0,0 +1,32 @@
import { Enum } from '../../../client/interfaces/Enum';
import { PrimaryType } from './constants';
export function getEnumFromDescription(description: string): Enum[] {
// Check if we can find this special format string:
// None=0,Something=1,AnotherThing=2
if (/^(\w+=[0-9]+,?)+$/g.test(description)) {
const matches = description.match(/(\w+=[0-9]+,?)/g);
if (matches) {
// Grab the values from the description
const symbols: Enum[] = [];
matches.forEach(match => {
const name = match.split('=')[0];
const value = parseInt(match.split('=')[1].replace(/[^0-9]/g, ''));
if (name && Number.isInteger(value)) {
symbols.push({
name: name.replace(/([a-z])([A-Z]+)/g, '$1_$2').toUpperCase(),
value: String(value),
type: PrimaryType.NUMBER,
});
}
});
// Filter out any duplicate names
return symbols.filter((symbol, index, arr) => {
return arr.map(item => item.name).indexOf(symbol.name) === index;
});
}
}
return [];
}

View File

@ -0,0 +1,18 @@
import { Enum } from '../../../client/interfaces/Enum';
import { getEnumValues } from './getEnumValues';
export function getEnumType(enumerators: Enum[], addParentheses: boolean = false): string {
// Fetch values from the symbols, just to be sure we filter out
// any double values and finally we sort them to make them easier
// to read when we use them in our generated code.
const values = getEnumValues(enumerators);
// Add grouping parentheses if needed. This can be handy if enum values
// are used in Arrays, so that you will get the following definition:
// const myArray: ('EnumValue1' | 'EnumValue2' | 'EnumValue3')[];
if (values.length > 1 && addParentheses) {
return `(${values.join(' | ')})`;
}
return values.join(' | ');
}

View File

@ -0,0 +1,13 @@
import { Enum } from '../../../client/interfaces/Enum';
export function getEnumValues(enumerators: Enum[]): string[] {
// Fetch values from the symbols, just to be sure we filter out
// any double values and finally we sort them to make them easier
// to read when we use them in our generated code.
return enumerators
.map(enumerator => enumerator.value)
.filter((enumerator, index, arr) => {
return arr.indexOf(enumerator) === index;
})
.sort();
}

View File

@ -0,0 +1,21 @@
import { getMappedType } from './getMappedType';
describe('getMappedType', () => {
it('should map types to the basics', () => {
expect(getMappedType('File')).toEqual('File');
expect(getMappedType('String')).toEqual('string');
expect(getMappedType('date')).toEqual('string');
expect(getMappedType('date-time')).toEqual('string');
expect(getMappedType('float')).toEqual('number');
expect(getMappedType('double')).toEqual('number');
expect(getMappedType('short')).toEqual('number');
expect(getMappedType('int')).toEqual('number');
expect(getMappedType('boolean')).toEqual('boolean');
expect(getMappedType('any')).toEqual('any');
expect(getMappedType('object')).toEqual('any');
expect(getMappedType('void')).toEqual('void');
expect(getMappedType('null')).toEqual('null');
expect(getMappedType('unknown')).toEqual('unknown');
expect(getMappedType('')).toEqual('');
});
});

View File

@ -0,0 +1,16 @@
import { PrimaryType, TYPE_MAPPINGS } from './constants';
/**
* Get mapped type for given type to any basic Typescript/Javascript type.
*/
export function getMappedType(type: string): PrimaryType | string {
const mapped = TYPE_MAPPINGS.get(type.toLowerCase());
if (mapped) {
return mapped;
}
return type;
}
export function hasMappedType(type: string): boolean {
return TYPE_MAPPINGS.has(type.toLowerCase());
}

View File

@ -0,0 +1,159 @@
import { OpenApi } from '../interfaces/OpenApi';
import { OpenApiSchema } from '../interfaces/OpenApiSchema';
import { getComment } from './getComment';
import { getType } from './getType';
import { Model } from '../../../client/interfaces/Model';
import { PrimaryType } from './constants';
import { getEnumType } from './getEnumType';
import { getEnum } from './getEnum';
import { getEnumFromDescription } from './getEnumFromDescription';
import { getModelProperties } from './getModelProperties';
export function getModel(openApi: OpenApi, definition: OpenApiSchema, isProperty: boolean = false, name: string = ''): Model {
const model: Model = {
name: name,
export: 'interface',
type: PrimaryType.OBJECT,
base: PrimaryType.OBJECT,
template: null,
link: null,
description: getComment(definition.description),
isProperty: isProperty,
isReadOnly: definition.readOnly || false,
isRequired: false,
isNullable: false,
imports: [],
extends: [],
enum: [],
enums: [],
properties: [],
};
if (definition.$ref) {
const definitionRef = getType(definition.$ref);
model.export = 'reference';
model.type = definitionRef.type;
model.base = definitionRef.base;
model.template = definitionRef.template;
model.imports.push(...definitionRef.imports);
return model;
}
if (definition.enum) {
const enumerators = getEnum(definition.enum);
if (enumerators.length) {
model.export = 'enum';
model.type = getEnumType(enumerators);
model.base = PrimaryType.STRING;
model.enum.push(...enumerators);
return model;
}
}
if ((definition.type === 'int' || definition.type === 'integer') && definition.description) {
const enumerators = getEnumFromDescription(definition.description);
if (enumerators.length) {
model.export = 'enum';
model.type = getEnumType(enumerators);
model.base = PrimaryType.NUMBER;
model.enum.push(...enumerators);
return model;
}
}
if (definition.type === 'array' && definition.items) {
if (definition.items.$ref) {
const arrayItems = getType(definition.items.$ref);
model.export = 'array';
model.type = arrayItems.type;
model.base = arrayItems.base;
model.template = arrayItems.template;
model.imports.push(...arrayItems.imports);
return model;
} else {
const arrayItems = getModel(openApi, definition.items, true);
model.export = 'array';
model.type = arrayItems.type;
model.base = arrayItems.base;
model.template = arrayItems.template;
model.link = arrayItems;
model.imports.push(...arrayItems.imports);
return model;
}
}
if (definition.type === 'object' && definition.additionalProperties && typeof definition.additionalProperties === 'object') {
if (definition.additionalProperties.$ref) {
const additionalProperties = getType(definition.additionalProperties.$ref);
model.export = 'dictionary';
model.type = additionalProperties.type;
model.base = additionalProperties.base;
model.template = additionalProperties.template;
model.imports.push(...additionalProperties.imports);
model.imports.push('Dictionary');
return model;
} else {
const additionalProperties = getModel(openApi, definition.additionalProperties);
model.export = 'dictionary';
model.type = additionalProperties.type;
model.base = additionalProperties.base;
model.template = additionalProperties.template;
model.link = additionalProperties;
model.imports.push(...additionalProperties.imports);
model.imports.push('Dictionary');
return model;
}
}
if (definition.type === 'object') {
model.export = 'interface';
model.type = PrimaryType.OBJECT;
model.base = PrimaryType.OBJECT;
if (definition.allOf) {
definition.allOf.forEach(parent => {
if (parent.$ref) {
const parentRef = getType(parent.$ref);
model.extends.push(parentRef.type);
model.imports.push(parentRef.base);
}
if (parent.type === 'object' && parent.properties) {
const properties = getModelProperties(openApi, parent);
properties.forEach(property => {
model.properties.push(property);
model.imports.push(...property.imports);
if (property.export === 'enum') {
model.enums.push(property);
}
});
}
});
}
if (definition.properties) {
const properties = getModelProperties(openApi, definition);
properties.forEach(property => {
model.properties.push(property);
model.imports.push(...property.imports);
if (property.export === 'enum') {
model.enums.push(property);
}
});
}
return model;
}
// If the schema has a type than it can be a basic or generic type.
if (definition.type) {
const definitionType = getType(definition.type);
model.export = 'generic';
model.type = definitionType.type;
model.base = definitionType.base;
model.template = definitionType.template;
model.imports.push(...definitionType.imports);
return model;
}
return model;
}

View File

@ -0,0 +1,60 @@
import { OpenApi } from '../interfaces/OpenApi';
import { OpenApiSchema } from '../interfaces/OpenApiSchema';
import { getComment } from './getComment';
import { getType } from './getType';
import { Model } from '../../../client/interfaces/Model';
import { getModel } from './getModel';
export function getModelProperties(openApi: OpenApi, definition: OpenApiSchema): Model[] {
const models: Model[] = [];
for (const propertyName in definition.properties) {
if (definition.properties.hasOwnProperty(propertyName)) {
const property = definition.properties[propertyName];
const propertyRequired = !!(definition.required && definition.required.includes(propertyName));
const propertyReadOnly = !!property.readOnly;
if (property.$ref) {
const model = getType(property.$ref);
models.push({
name: propertyName,
export: 'reference',
type: model.type,
base: model.base,
template: model.template,
link: null,
description: getComment(property.description),
isProperty: true,
isReadOnly: propertyReadOnly,
isRequired: propertyRequired,
isNullable: false,
imports: model.imports,
extends: [],
enum: [],
enums: [],
properties: [],
});
} else {
const model = getModel(openApi, property);
models.push({
name: propertyName,
export: model.export,
type: model.type,
base: model.base,
template: model.template,
link: model.link,
description: getComment(property.description),
isProperty: true,
isReadOnly: propertyReadOnly,
isRequired: propertyRequired,
isNullable: false,
imports: model.imports,
extends: model.extends,
enum: model.enum,
enums: model.enums,
properties: model.properties,
});
}
}
}
return models;
}

View File

@ -0,0 +1,23 @@
import { getModelTemplate } from './getModelTemplate';
describe('getModelTemplate', () => {
it('should return generic for template type', () => {
const template = getModelTemplate({
type: 'Link<Model>',
base: 'Link',
template: 'Model',
imports: ['Model'],
});
expect(template).toEqual('<T>');
});
it('should return empty for primary type', () => {
const template = getModelTemplate({
type: 'string',
base: 'string',
template: null,
imports: [],
});
expect(template).toEqual('');
});
});

View File

@ -0,0 +1,11 @@
import { Type } from '../../../client/interfaces/Type';
/**
* If our model has a template type, then we want to generalize that!
* In that case we should return "<T>" as our template type.
* @param modelClass The parsed model class type.
* @returns The model template type (<T> or empty).
*/
export function getModelTemplate(modelClass: Type): string {
return modelClass.template ? '<T>' : '';
}

View File

@ -1,11 +1,19 @@
import { Model } from '../../../client/interfaces/Model';
import { OpenApi } from '../interfaces/OpenApi';
import {Model} from '../../../client/interfaces/Model';
import {OpenApi} from '../interfaces/OpenApi';
import {getModel} from './getModel';
import {getType} from './getType';
/**
* Parse and return the OpenAPI models.
* @param openApi
*/
export function getModels(openApi: OpenApi): Map<string, Model> {
const models = new Map<string, Model>();
if (openApi.components) {
for (const definitionName in openApi.components.schemas) {
if (openApi.components.schemas.hasOwnProperty(definitionName)) {
const definition = openApi.components.schemas[definitionName];
const definitionType = getType(definitionName);
const model = getModel(openApi, definition, false, definitionType.base);
models.set(definitionType.base, model);
}
}
}
return models;
}

View File

@ -0,0 +1,65 @@
import { Service } from '../../../client/interfaces/Service';
import { getServiceClassName } from './getServiceClassName';
import { OpenApiOperation } from '../interfaces/OpenApiOperation';
import { getOperationName } from './getOperationName';
import { getOperationPath } from './getOperationPath';
import { OpenApi } from '../interfaces/OpenApi';
import { getComment } from './getComment';
import { Operation } from '../../../client/interfaces/Operation';
import { getOperationParameters } from './getOperationParameters';
import { getOperationResponses } from './getOperationResponses';
import { getOperationResults } from './getOperationResults';
import { getOperationErrors } from './getOperationErrors';
export function getOperation(openApi: OpenApi, url: string, method: string, op: OpenApiOperation): Operation {
const serviceName = (op.tags && op.tags[0]) || 'Service';
const serviceClassName = getServiceClassName(serviceName);
const operationNameFallback = `${method}${serviceClassName}`;
const operationName = getOperationName(op.operationId || operationNameFallback);
const operationPath = getOperationPath(url);
// Create a new operation object for this method.
const operation: Operation = {
service: serviceClassName,
name: operationName,
summary: getComment(op.summary),
description: getComment(op.description),
deprecated: op.deprecated || false,
method: method,
path: operationPath,
parameters: [],
parametersPath: [],
parametersQuery: [],
parametersForm: [],
parametersHeader: [],
parametersBody: null,
imports: [],
errors: [],
results: [],
};
// Parse the operation parameters (path, query, body, etc).
if (op.parameters) {
const parameters = getOperationParameters(openApi, op.parameters);
operation.imports.push(...parameters.imports);
operation.parameters.push(...parameters.parameters);
operation.parametersPath.push(...parameters.parametersPath);
operation.parametersQuery.push(...parameters.parametersQuery);
operation.parametersForm.push(...parameters.parametersForm);
operation.parametersHeader.push(...parameters.parametersHeader);
operation.parametersBody = parameters.parametersBody;
}
// Parse the operation responses.
if (op.responses) {
const operationResponses = getOperationResponses(openApi, op.responses);
const operationResults = getOperationResults(operationResponses);
operation.errors = getOperationErrors(operationResponses);
operationResults.forEach(operationResult => {
operation.results.push(operationResult);
operation.imports.push(...operationResult.imports);
});
}
return operation;
}

View File

@ -0,0 +1,13 @@
import { OperationResponse } from '../../../client/interfaces/OperationResponse';
import { OperationError } from '../../../client/interfaces/OperationError';
export function getOperationErrors(operationResponses: OperationResponse[]): OperationError[] {
return operationResponses
.filter(operationResponse => {
return operationResponse.code >= 300 && operationResponse.description;
})
.map(response => ({
code: response.code,
description: response.description!,
}));
}

View File

@ -0,0 +1,10 @@
import { getOperationName } from './getOperationName';
describe('getOperationName', () => {
it('should produce correct result', () => {
expect(getOperationName('')).toEqual('');
expect(getOperationName('FooBar')).toEqual('fooBar');
expect(getOperationName('Foo Bar')).toEqual('fooBar');
expect(getOperationName('foo bar')).toEqual('fooBar');
});
});

View File

@ -0,0 +1,11 @@
import camelCase from 'camelcase';
/**
* Convert the input value to a correct operation (method) classname.
* This converts the input string to camelCase, so the method name follows
* the most popular Javascript and Typescript writing style.
*/
export function getOperationName(value: string): string {
const clean = value.replace(/[^\w\s\-]+/g, '_').trim();
return camelCase(clean);
}

View File

@ -0,0 +1,59 @@
import { OpenApiParameter } from '../interfaces/OpenApiParameter';
import { OpenApi } from '../interfaces/OpenApi';
import { getComment } from './getComment';
import { getOperationParameterName } from './getOperationParameterName';
import { OperationParameter } from '../../../client/interfaces/OperationParameter';
import { PrimaryType } from './constants';
import { getType } from './getType';
import { getModel } from './getModel';
export function getOperationParameter(openApi: OpenApi, parameter: OpenApiParameter): OperationParameter {
const operationParameter: OperationParameter = {
in: parameter.in,
prop: parameter.name,
export: 'interface',
name: getOperationParameterName(parameter.name),
type: PrimaryType.OBJECT,
base: PrimaryType.OBJECT,
template: null,
link: null,
description: getComment(parameter.description),
default: undefined,
isProperty: false,
isReadOnly: false,
isRequired: parameter.required || false,
isNullable: false,
imports: [],
extends: [],
enum: [],
enums: [],
properties: [],
};
if (parameter.schema) {
if (parameter.schema.$ref) {
const model = getType(parameter.schema.$ref);
operationParameter.export = 'reference';
operationParameter.type = model.type;
operationParameter.base = model.base;
operationParameter.template = model.template;
operationParameter.imports.push(...model.imports);
return operationParameter;
} else {
const model = getModel(openApi, parameter.schema);
operationParameter.export = model.export;
operationParameter.type = model.type;
operationParameter.base = model.base;
operationParameter.template = model.template;
operationParameter.link = model.link;
operationParameter.imports.push(...model.imports);
operationParameter.extends.push(...model.extends);
operationParameter.enum.push(...model.enum);
operationParameter.enums.push(...model.enums);
operationParameter.properties.push(...model.properties);
return operationParameter;
}
}
return operationParameter;
}

View File

@ -0,0 +1,15 @@
export function getOperationParameterDefault(value: any): string | null {
if (value === null) {
return 'null';
}
switch (typeof value) {
case 'number':
case 'boolean':
return JSON.stringify(value);
case 'string':
return `'${value}'`;
}
return null;
}

View File

@ -0,0 +1,10 @@
import camelCase from 'camelcase';
/**
* Replaces any invalid characters from a parameter name.
* For example: 'filter.someProperty' becomes 'filterSomeProperty'.
*/
export function getOperationParameterName(value: string): string {
const clean = value.replace(/[^\w\s\-]+/g, '_').trim();
return camelCase(clean);
}

View File

@ -0,0 +1,59 @@
import { OpenApiParameter } from '../interfaces/OpenApiParameter';
import { OpenApi } from '../interfaces/OpenApi';
import { getRef } from './getRef';
import { OperationParameters } from '../../../client/interfaces/OperationParameters';
import { OperationParameter } from '../../../client/interfaces/OperationParameter';
import { getOperationParameter } from './getOperationParameter';
function sortByRequired(a: OperationParameter, b: OperationParameter): number {
return a.isRequired && !b.isRequired ? -1 : !a.isRequired && b.isRequired ? 1 : 0;
}
export function getOperationParameters(openApi: OpenApi, parameters: OpenApiParameter[]): OperationParameters {
const operationParameters: OperationParameters = {
imports: [],
parameters: [],
parametersPath: [],
parametersQuery: [],
parametersForm: [],
parametersHeader: [],
parametersBody: null,
};
// Iterate over the parameters
parameters.forEach(parameter => {
const paramRef = getRef<OpenApiParameter>(openApi, parameter);
const param = getOperationParameter(openApi, paramRef);
// We ignore the "api-version" param, since we do not want to add this
// as the first / default parameter for each of the service calls.
if (param.prop !== 'api-version') {
switch (parameter.in) {
case 'path':
operationParameters.parametersPath.push(param);
operationParameters.parameters.push(param);
operationParameters.imports.push(...param.imports);
break;
case 'query':
operationParameters.parametersQuery.push(param);
operationParameters.parameters.push(param);
operationParameters.imports.push(...param.imports);
break;
case 'header':
operationParameters.parametersHeader.push(param);
operationParameters.parameters.push(param);
operationParameters.imports.push(...param.imports);
break;
}
}
});
operationParameters.parameters = operationParameters.parameters.sort(sortByRequired);
operationParameters.parametersPath = operationParameters.parametersPath.sort(sortByRequired);
operationParameters.parametersQuery = operationParameters.parametersQuery.sort(sortByRequired);
operationParameters.parametersForm = operationParameters.parametersForm.sort(sortByRequired);
operationParameters.parametersHeader = operationParameters.parametersHeader.sort(sortByRequired);
return operationParameters;
}

View File

@ -0,0 +1,10 @@
import { getOperationPath } from './getOperationPath';
describe('getOperationPath', () => {
it('should produce correct result', () => {
expect(getOperationPath('/api/v{api-version}/list/{id}/{type}')).toEqual('/api/v${OpenAPI.VERSION}/list/${id}/${type}');
expect(getOperationPath('/api/v{api-version}/list/{id}')).toEqual('/api/v${OpenAPI.VERSION}/list/${id}');
expect(getOperationPath('/api/v1/list/{id}')).toEqual('/api/v1/list/${id}');
expect(getOperationPath('/api/v1/list')).toEqual('/api/v1/list');
});
});

View File

@ -0,0 +1,9 @@
/**
* Get the final service path, this replaces the "{api-version}" placeholder
* with a new template string placeholder so we can dynamically inject the
* OpenAPI version without the need to hardcode this in the URL.
* @param path
*/
export function getOperationPath(path: string): string {
return path.replace(/{api-version}/g, '{OpenAPI.VERSION}').replace(/\{(.*?)\}/g, '${$1}');
}

View File

@ -0,0 +1,16 @@
export function getOperationResponseCode(value: string | 'default'): number | null {
// You can specify a "default" response, this is treated as HTTP code 200
if (value === 'default') {
return 200;
}
// Check if we can parse the code and return of successful.
if (/[0-9]+/g.test(value)) {
const code = parseInt(value);
if (Number.isInteger(code)) {
return code;
}
}
return null;
}

View File

@ -0,0 +1,86 @@
import { OpenApiResponses } from '../interfaces/OpenApiResponses';
import { getOperationResponseCode } from './getOperationResponseCode';
import { OpenApiResponse } from '../interfaces/OpenApiResponse';
import { getRef } from './getRef';
import { OpenApi } from '../interfaces/OpenApi';
import { OperationResponse } from '../../../client/interfaces/OperationResponse';
import { getType } from './getType';
import { getModel } from './getModel';
import { getComment } from './getComment';
import { PrimaryType } from './constants';
export function getOperationResponses(openApi: OpenApi, responses: OpenApiResponses): OperationResponse[] {
const operationResponses: OperationResponse[] = [];
// Iterate over each response code and get the
// status code and response message (if any).
for (const code in responses) {
if (responses.hasOwnProperty(code)) {
const responseOrReference = responses[code];
const response = getRef<OpenApiResponse>(openApi, responseOrReference);
const responseCode = getOperationResponseCode(code);
// If there is a response code then we check what data we get back,
// if there is no typed data, we just return <any> so the user is still
// free to do their own casting if needed.
if (responseCode) {
const operationResponse: OperationResponse = {
name: '',
code: responseCode,
description: getComment(response.description)!,
export: 'generic',
type: PrimaryType.OBJECT,
base: PrimaryType.OBJECT,
template: null,
link: null,
isProperty: false,
isReadOnly: false,
isRequired: false,
isNullable: false,
imports: [],
extends: [],
enum: [],
enums: [],
properties: [],
};
// If this response has a schema, then we need to check two things:
// if this is a reference then the parameter is just the 'name' of
// this reference type. Otherwise it might be a complex schema and
// then we need to parse the schema!
// TODO: Needs content!
// if (response.schema) {
// if (response.schema.$ref) {
// const model = getType(response.schema.$ref);
// operationResponse.export = 'reference';
// operationResponse.type = model.type;
// operationResponse.base = model.base;
// operationResponse.template = model.template;
// operationResponse.imports.push(...model.imports);
// } else {
// const model = getModel(openApi, response.schema);
// operationResponse.export = model.export;
// operationResponse.type = model.type;
// operationResponse.base = model.base;
// operationResponse.template = model.template;
// operationResponse.link = model.link;
// operationResponse.imports.push(...model.imports);
// operationResponse.extends.push(...model.extends);
// operationResponse.enum.push(...model.enum);
// operationResponse.enums.push(...model.enums);
// operationResponse.properties.push(...model.properties);
// }
// }
operationResponses.push(operationResponse);
}
}
}
// Sort the responses to 2XX success codes come before 4XX and 5XX error codes.
return operationResponses.sort((a, b): number => {
return a.code < b.code ? -1 : a.code > b.code ? 1 : 0;
});
}

View File

@ -0,0 +1,51 @@
import { PrimaryType } from './constants';
import { OperationResponse } from '../../../client/interfaces/OperationResponse';
import { Model } from '../../../client/interfaces/Model';
function areEqual(a: Model, b: Model): boolean {
const equal = a.type === b.type && a.base === b.base && a.template === b.template;
if (equal && a.link && b.link) {
return areEqual(a.link, b.link);
}
return equal;
}
export function getOperationResults(operationResponses: OperationResponse[]): OperationResponse[] {
const operationResults: OperationResponse[] = [];
operationResponses.forEach(operationResponse => {
if (operationResponse.code && operationResponse.code >= 200 && operationResponse.code < 300) {
operationResults.push(operationResponse);
}
});
if (!operationResults.length) {
operationResults.push({
name: '',
code: 200,
description: '',
export: 'interface',
type: PrimaryType.OBJECT,
base: PrimaryType.OBJECT,
template: null,
link: null,
isProperty: false,
isReadOnly: false,
isRequired: false,
isNullable: false,
imports: [],
extends: [],
enum: [],
enums: [],
properties: [],
});
}
return operationResults.filter((operationResult, index, arr) => {
return (
arr.findIndex(item => {
return areEqual(item, operationResult);
}) === index
);
});
}

View File

@ -0,0 +1,26 @@
import { OpenApi } from '../interfaces/OpenApi';
import { OpenApiReference } from '../interfaces/OpenApiReference';
export function getRef<T>(openApi: OpenApi, item: T & OpenApiReference): T {
if (item.$ref) {
// Fetch the paths to the definitions, this converts:
// "#/definitions/Form" to ["definitions", "Form"]
const paths = item.$ref
.replace(/^#/g, '')
.split('/')
.filter(item => item);
// Try to find the reference by walking down the path,
// if we cannot find it, then we throw an error.
let result: any = openApi;
paths.forEach((path: string): void => {
if (result.hasOwnProperty(path)) {
result = result[path];
} else {
throw new Error(`Could not find reference: "${item.$ref}"`);
}
});
return result as T;
}
return item as T;
}

View File

@ -0,0 +1,13 @@
import { getServiceClassName } from './getServiceClassName';
describe('getServiceClassName', () => {
it('should produce correct result', () => {
expect(getServiceClassName('')).toEqual('');
expect(getServiceClassName('FooBar')).toEqual('FooBarService');
expect(getServiceClassName('Foo Bar')).toEqual('FooBarService');
expect(getServiceClassName('foo bar')).toEqual('FooBarService');
expect(getServiceClassName('FooBarService')).toEqual('FooBarService');
expect(getServiceClassName('Foo Bar Service')).toEqual('FooBarService');
expect(getServiceClassName('foo bar service')).toEqual('FooBarService');
});
});

View File

@ -0,0 +1,14 @@
import camelCase from 'camelcase';
/**
* Convert the input value to a correct service classname. This converts
* the input string to PascalCase and appends the "Service" prefix if needed.
*/
export function getServiceClassName(value: string): string {
const clean = value.replace(/[^\w\s\-]+/g, '_').trim();
const name = camelCase(clean, { pascalCase: true });
if (name && !name.endsWith('Service')) {
return `${name}Service`;
}
return name;
}

View File

@ -1,11 +1,49 @@
import { Service } from '../../../client/interfaces/Service';
import { OpenApi } from '../interfaces/OpenApi';
import { Method } from './constants';
import { getOperation } from './getOperation';
/**
* Parse and return the OpenAPI services.
* @param openApi
* Get the OpenAPI services
*/
export function getServices(openApi: OpenApi): Map<string, Service> {
const services = new Map<string, Service>();
for (const url in openApi.paths) {
if (openApi.paths.hasOwnProperty(url)) {
const path = openApi.paths[url];
for (const method in path) {
if (path.hasOwnProperty(method)) {
switch (method) {
case Method.GET:
case Method.PUT:
case Method.POST:
case Method.DELETE:
case Method.OPTIONS:
case Method.HEAD:
case Method.PATCH:
// Each method contains an OpenAPI operation, we parse the operation
const op = path[method]!;
const operation = getOperation(openApi, url, method, op);
// If we have already declared a service, then we should fetch that and
// append the new method to it. Otherwise we should create a new service object.
const service =
services.get(operation.service) ||
({
name: operation.service,
operations: [],
imports: [],
} as Service);
// Push the operation in the service
service.operations.push(operation);
service.imports.push(...operation.imports);
services.set(operation.service, service);
break;
}
}
}
}
}
return services;
}

View File

@ -0,0 +1,59 @@
import { getType } from './getType';
describe('getType', () => {
it('should convert int', () => {
const type = getType('int');
expect(type.type).toEqual('number');
expect(type.base).toEqual('number');
expect(type.template).toEqual(null);
expect(type.imports).toEqual([]);
});
it('should convert string', () => {
const type = getType('String');
expect(type.type).toEqual('string');
expect(type.base).toEqual('string');
expect(type.template).toEqual(null);
expect(type.imports).toEqual([]);
});
it('should convert string array', () => {
const type = getType('Array[String]');
expect(type.type).toEqual('Array<string>');
expect(type.base).toEqual('Array');
expect(type.template).toEqual('string');
expect(type.imports).toEqual(['Array']);
});
it('should convert template with primary', () => {
const type = getType('#/definitions/Link[String]');
expect(type.type).toEqual('Link<string>');
expect(type.base).toEqual('Link');
expect(type.template).toEqual('string');
expect(type.imports).toEqual(['Link']);
});
it('should convert template with model', () => {
const type = getType('#/definitions/Link[Model]');
expect(type.type).toEqual('Link<Model>');
expect(type.base).toEqual('Link');
expect(type.template).toEqual('Model');
expect(type.imports).toEqual(['Link', 'Model']);
});
it('should have double imports', () => {
const type = getType('#/definitions/Link[Link]');
expect(type.type).toEqual('Link<Link>');
expect(type.base).toEqual('Link');
expect(type.template).toEqual('Link');
expect(type.imports).toEqual(['Link', 'Link']);
});
it('should convert generic', () => {
const type = getType('#/definitions/Link', 'Link');
expect(type.type).toEqual('T');
expect(type.base).toEqual('T');
expect(type.template).toEqual(null);
expect(type.imports).toEqual([]);
});
});

View File

@ -0,0 +1,59 @@
import { stripNamespace } from './stripNamespace';
import { Type } from '../../../client/interfaces/Type';
import { getMappedType, hasMappedType } from './getMappedType';
import { PrimaryType } from './constants';
/**
* Parse any string value into a type object.
* @param value String value like "integer" or "Link[Model]".
* @param template Optional template class from parent (needed to process generics)
*/
export function getType(value?: string, template?: string): Type {
const result: Type = {
type: PrimaryType.OBJECT,
base: PrimaryType.OBJECT,
template: null,
imports: [],
};
const valueClean = stripNamespace(value || '');
if (/\[.*\]$/g.test(valueClean)) {
const matches = valueClean.match(/(.*?)\[(.*)\]$/);
if (matches && matches.length) {
const match1 = getType(matches[1]);
const match2 = getType(matches[2]);
if (match2.type) {
result.type = `${match1.type}<${match2.type}>`;
result.base = match1.type;
result.template = match2.type;
} else {
result.type = match1.type;
result.base = match1.type;
result.template = match1.type;
}
result.imports.push(...match1.imports);
result.imports.push(...match2.imports);
}
} else if (hasMappedType(valueClean)) {
const mapped = getMappedType(valueClean);
result.type = mapped;
result.base = mapped;
} else if (valueClean) {
result.type = valueClean;
result.base = valueClean;
result.imports.push(valueClean);
}
// If the property that we found matched the parent template class
// Then ignore this whole property and return it as a "T" template property.
if (result.type === template) {
result.type = 'T'; // Template;
result.base = 'T'; // Template;
result.imports = [];
}
return result;
}

View File

@ -0,0 +1,14 @@
import { isPrimaryType } from './isPrimaryType';
describe('isPrimaryType', () => {
it('should return true for primary types', () => {
expect(isPrimaryType('number')).toBeTruthy();
expect(isPrimaryType('boolean')).toBeTruthy();
expect(isPrimaryType('string')).toBeTruthy();
expect(isPrimaryType('any')).toBeTruthy();
expect(isPrimaryType('void')).toBeTruthy();
expect(isPrimaryType('null')).toBeTruthy();
expect(isPrimaryType('Array')).toBeFalsy();
expect(isPrimaryType('MyModel')).toBeFalsy();
});
});

View File

@ -0,0 +1,19 @@
import { PrimaryType } from './constants';
/**
* Check if given type is a primary type.
* @param type
*/
export function isPrimaryType(type: string): type is PrimaryType {
switch (type.toLowerCase()) {
case PrimaryType.FILE:
case PrimaryType.OBJECT:
case PrimaryType.BOOLEAN:
case PrimaryType.NUMBER:
case PrimaryType.STRING:
case PrimaryType.VOID:
case PrimaryType.NULL:
return true;
}
return false;
}

View File

@ -7,7 +7,7 @@
{{#if properties}}
yup.object{{#unless isProperty}}{{#if name}}<{{{name}}}>{{/if}}{{/unless}}().shape({
{{#each properties}}
{{{name}}}: yup.lazy(() => {{>validation}}.default(undefined){{#if isNullable}}.isNullable(){{/if}}){{#if isRequired}}.isRequired(){{/if}}{{#unless @last}},{{/unless}}
{{{name}}}: yup.lazy(() => {{>validation}}.default(undefined){{#if isNullable}}.nullable(){{/if}}{{#if isRequired}}.required(){{/if}}){{#unless @last}},{{/unless}}
{{/each}}
}).noUnknown()
{{else}}

View File

@ -1,17 +1,24 @@
const OpenAPI = require('../dist');
OpenAPI.generate(
'./test/mock/spec-v2.json',
'./test/result/v2/typescript/',
'./test/mock/spec-v3.json',
'./test/result/v3/typescript/',
OpenAPI.Language.TYPESCRIPT,
OpenAPI.HttpClient.FETCH,
);
OpenAPI.generate(
'./test/mock/spec-v2.json',
'./test/result/v2/javascript/',
OpenAPI.Language.JAVASCRIPT,
OpenAPI.HttpClient.XHR,
);
// OpenAPI.generate(
// './test/mock/spec-v2.json',
// './test/result/v2/typescript/',
// OpenAPI.Language.TYPESCRIPT,
// OpenAPI.HttpClient.FETCH,
// );
OpenAPI.compile('./test/result/v2/typescript/');
// OpenAPI.generate(
// './test/mock/spec-v2.json',
// './test/result/v2/javascript/',
// OpenAPI.Language.JAVASCRIPT,
// OpenAPI.HttpClient.XHR,
// );
// OpenAPI.compile('./test/result/v2/typescript/');

File diff suppressed because it is too large Load Diff