Skip to content

Commit

Permalink
feat: Interfaces generation (#71)
Browse files Browse the repository at this point in the history
  • Loading branch information
ffflorian authored Aug 13, 2019
1 parent ff76562 commit 59e5649
Show file tree
Hide file tree
Showing 11 changed files with 505 additions and 115 deletions.
19 changes: 19 additions & 0 deletions src/Swaxios.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,25 @@ describe('writeClient', () => {

expect(actual).toBe(expected);
});

it('supports definitions', async () => {
const inputFile = path.resolve(__dirname, './test/snapshots/8-definitions.json');
await writeClient(inputFile, tempDir, true);

let actual = await fs.readFile(path.join(tempDir, 'interfaces/CreateAccountRequest.ts'), 'utf-8');
let expected = await fs.readFile(
path.resolve(__dirname, './test/snapshots/8-definitions-CreateAccountRequest.ts.fixture'),
'utf-8',
);
expect(actual).toBe(expected);

actual = await fs.readFile(path.join(tempDir, 'interfaces/CreateAccountResponse.ts'), 'utf-8');
expected = await fs.readFile(
path.resolve(__dirname, './test/snapshots/8-definitions-CreateAccountResponse.ts.fixture'),
'utf-8',
);
expect(actual).toBe(expected);
});
});

describe('exportServices', () => {
Expand Down
24 changes: 24 additions & 0 deletions src/Swaxios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import yaml from 'yamljs';

import {OpenAPIV2} from 'openapi-types';
import {APIClientGenerator, IndexFileGenerator, ResourceGenerator} from './generators';
import {InterfaceGenerator} from './generators/InterfaceGenerator';
import {DirEntry, generateFileIndex} from './util/FileUtil';
import * as StringUtil from './util/StringUtil';
import {validateConfig} from './validator/SwaggerValidator';
Expand Down Expand Up @@ -65,7 +66,30 @@ export async function generateClient(swaggerJson: OpenAPIV2.Document, outputDire
name: 'APIClient',
};

fileIndex.directories.interfaces = {
directories: {},
files: {},
fullPath: path.resolve(outputDirectory, 'interfaces'),
name: 'interfaces',
};

await buildIndexFiles(fileIndex);
await generateInterfaces(swaggerJson, outputDirectory);
}

async function generateInterfaces(spec: OpenAPIV2.Document, outputDirectory: string): Promise<void> {
if (!spec.definitions) {
console.info('Spec has no definitions.');
return;
}

const interfaceDirectory = path.join(outputDirectory, 'interfaces');

for (const [definitionName, definition] of Object.entries(spec.definitions)) {
await new InterfaceGenerator(definitionName, definition, spec, interfaceDirectory).write();
}

await new IndexFileGenerator(Object.keys(spec.definitions), interfaceDirectory).write();
}

function parseInputFile(inputFile: string): OpenAPIV2.Document {
Expand Down
213 changes: 213 additions & 0 deletions src/generators/InterfaceGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import fs from 'fs-extra';
import {OpenAPIV2} from 'openapi-types';
import path from 'path';
import {inspect} from 'util';

import {GeneratorContext, TemplateGenerator} from './TemplateGenerator';

export enum SwaggerType {
ARRAY = 'array',
BOOLEAN = 'boolean',
INTEGER = 'integer',
NUMBER = 'number',
OBJECT = 'object',
STRING = 'string',
}

export enum TypeScriptType {
ANY = 'any',
ARRAY = 'Array',
BOOLEAN = 'boolean',
EMPTY_OBJECT = '{}',
NUMBER = 'number',
STRING = 'string',
INTERFACE = 'interface',
TYPE = 'type',
}

interface SwaxiosInterface {
basicType?: TypeScriptType;
type: string;
imports: string[];
}

interface Context extends GeneratorContext {
basicType?: string;
imports: string[];
name: string;
typeData: string;
}

export class InterfaceGenerator extends TemplateGenerator {
protected readonly name: string;
protected readonly templateFile: string;
private readonly spec: OpenAPIV2.Document;
private readonly definition: OpenAPIV2.Schema;
private readonly outputDirectory: string;

constructor(definitionName: string, definition: OpenAPIV2.Schema, spec: OpenAPIV2.Document, outputDirectory: string) {
super();
this.name = definitionName;
this.templateFile = 'Interface.hbs';
this.spec = spec;
this.definition = definition;
this.outputDirectory = outputDirectory;

fs.ensureDirSync(this.outputDirectory);
}

public static buildInterface(
spec: OpenAPIV2.Document,
schema: OpenAPIV2.Schema,
schemaName: string,
imports: string[] = [],
basicType: TypeScriptType = TypeScriptType.TYPE,
): SwaxiosInterface {
const reference = (schema as OpenAPIV2.ReferenceObject).$ref;

if (reference && reference.startsWith('#/definitions')) {
if (!spec.definitions) {
console.info('Spec has no definitions.');
return {type: TypeScriptType.EMPTY_OBJECT, imports};
}
const definition = reference.replace('#/definitions/', '');
if (!imports.includes(definition)) {
imports.push(definition);
}
return {type: definition, imports};
}

const schemaObject = schema as OpenAPIV2.SchemaObject;
const {allOf: multipleSchemas, required: requiredProperties, properties} = schemaObject;

if (multipleSchemas) {
const multipleTypes = multipleSchemas.map(itemSchema =>
InterfaceGenerator.buildInterface(spec, itemSchema as OpenAPIV2.Schema, schemaName, imports, basicType),
);

const schemas = multipleTypes.map(item => item.type).join('&');

for (const itemType of multipleTypes) {
for (const itemImport of itemType.imports) {
if (!imports.includes(itemImport)) {
imports.push(itemImport);
}
}
}

return {basicType, type: schemas, imports};
}

let schemaType = schemaObject.type || SwaggerType.OBJECT;

if (Array.isArray(schemaType)) {
schemaType = schemaType[0];
}

switch (schemaType.toLowerCase()) {
case SwaggerType.BOOLEAN: {
return {basicType, type: TypeScriptType.BOOLEAN, imports};
}
case SwaggerType.STRING: {
return {basicType, type: TypeScriptType.STRING, imports};
}
case SwaggerType.NUMBER:
case SwaggerType.INTEGER: {
return {basicType, type: TypeScriptType.NUMBER, imports};
}
case SwaggerType.OBJECT: {
if (!properties) {
console.info(`Schema type for "${schemaName}" is "object" but has no properties.`);
return {basicType: TypeScriptType.INTERFACE, type: TypeScriptType.EMPTY_OBJECT, imports};
}

const schema: Record<string, string> = {};

for (const [property, propertyOptions] of Object.entries(properties)) {
const isRequired = requiredProperties && requiredProperties.includes(property);
const isReadOnly = propertyOptions.readOnly;
const propertyName = `${isReadOnly ? 'readonly ' : ''}${property}${isRequired ? '' : '?'}`;
const {type: propertyType, imports: propertyImports} = InterfaceGenerator.buildInterface(
spec,
propertyOptions,
property,
imports,
);
schema[propertyName] = propertyType;
for (const propertyImport of propertyImports) {
if (!imports.includes(propertyImport)) {
imports.push(propertyImport);
}
}
}

const type = inspect(schema, {breakLength: Infinity})
.replace(/'/gm, '')
.replace(',', ';')
.replace(new RegExp('\\n', 'g'), '');
return {basicType: TypeScriptType.INTERFACE, type, imports};
}
case SwaggerType.ARRAY: {
if (!schemaObject.items) {
console.info(`Schema type for "${schemaName}" is "array" but has no items.`);
return {basicType, type: `${TypeScriptType.ARRAY}<${TypeScriptType.ANY}>`, imports};
}

if (!(schemaObject.items instanceof Array)) {
const {imports: itemImports, type: itemType} = InterfaceGenerator.buildInterface(
spec,
schemaObject.items,
schemaName,
imports,
);
for (const itemImport of itemImports) {
if (!imports.includes(itemImport)) {
imports.push(itemImport);
}
}
return {basicType, type: `${TypeScriptType.ARRAY}<${itemType}>`, imports};
}

const itemTypes = schemaObject.items.map(itemSchema =>
InterfaceGenerator.buildInterface(spec, itemSchema, schemaName, imports, basicType),
);

const schemas = itemTypes.map(item => item.type).join('|');

for (const itemType of itemTypes) {
for (const itemImport of itemType.imports) {
if (!imports.includes(itemImport)) {
imports.push(itemImport);
}
}
}

return {basicType, type: `${TypeScriptType.ARRAY}<${schemas}>`, imports};
}
default: {
return {basicType, type: TypeScriptType.EMPTY_OBJECT, imports};
}
}
}

generateInterface(): SwaxiosInterface {
return InterfaceGenerator.buildInterface(this.spec, this.definition, this.name);
}

async write(): Promise<void> {
const renderedIndex = await this.toString();
const outputFile = path.join(this.outputDirectory, this.filePath);
return fs.outputFile(outputFile, renderedIndex, 'utf-8');
}

protected async getContext(): Promise<Context> {
const {basicType, imports, type: typeData} = this.generateInterface();

return {
basicType: basicType || '',
imports,
name: this.name,
typeData,
};
}
}
Loading

0 comments on commit 59e5649

Please sign in to comment.