Skip to content

Commit

Permalink
refactor(api): minor refactors to how some internal language classes …
Browse files Browse the repository at this point in the history
…are used (#755)
  • Loading branch information
erunion authored Oct 11, 2023
1 parent f65bc79 commit 518736d
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface InstallerOptions {
logger?: (msg: string) => void;
}

export default abstract class CodeGeneratorLanguage {
export default abstract class CodeGenerator {
spec: Oas;

specPath: string;
Expand Down Expand Up @@ -55,9 +55,9 @@ export default abstract class CodeGeneratorLanguage {
}
}

abstract generator(): Promise<Record<string, string>>;
abstract compile(): Promise<Record<string, string>>;

abstract installer(storage: Storage, opts?: InstallerOptions): Promise<void>;
abstract install(storage: Storage, opts?: InstallerOptions): Promise<void>;

hasRequiredPackages() {
return Boolean(Object.keys(this.requiredPackages));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import type CodeGeneratorLanguage from './language.js';
import type CodeGenerator from './codegenerator.js';
import type Oas from 'oas';

import TSGenerator from './languages/typescript.js';
import TSGenerator from './languages/typescript/index.js';

export type SupportedLanguages = 'js' | 'js-cjs' | 'js-esm' | 'ts';

export default function codegen(
export default function codegenFactory(
language: SupportedLanguages,
spec: Oas,
specPath: string,
identifier: string,
): CodeGeneratorLanguage {
): CodeGenerator {
switch (language) {
case 'js':
throw new TypeError('An export format of CommonJS or ECMAScript is required for JavaScript compilation.');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type Storage from '../../storage.js';
import type { InstallerOptions } from '../language.js';
import type Storage from '../../../storage.js';
import type { InstallerOptions } from '../../codegenerator.js';
import type Oas from 'oas';
import type Operation from 'oas/operation';
import type { HttpMethods, SchemaObject } from 'oas/rmoas.types';
Expand All @@ -21,10 +21,10 @@ import setWith from 'lodash.setwith';
import semver from 'semver';
import { IndentationText, Project, QuoteKind, ScriptTarget, VariableDeclarationKind } from 'ts-morph';

import logger from '../../logger.js';
import CodeGeneratorLanguage from '../language.js';
import logger from '../../../logger.js';
import CodeGenerator from '../../codegenerator.js';

import { docblockEscape, generateTypeName, wordWrap } from './typescript/util.js';
import { docblockEscape, generateTypeName, wordWrap } from './util.js';

export interface TSGeneratorOptions {
compilerTarget?: 'cjs' | 'esm';
Expand All @@ -45,7 +45,7 @@ interface OperationTypeHousing {
};
}

export default class TSGenerator extends CodeGeneratorLanguage {
export default class TSGenerator extends CodeGenerator {
project: Project;

outputJS: boolean;
Expand Down Expand Up @@ -129,7 +129,7 @@ export default class TSGenerator extends CodeGeneratorLanguage {
this.schemas = {};
}

async installer(storage: Storage, opts: InstallerOptions = {}): Promise<void> {
async install(storage: Storage, opts: InstallerOptions = {}): Promise<void> {
const installDir = storage.getIdentifierStorageDir();

const info = this.spec.getDefinition().info;
Expand Down Expand Up @@ -185,7 +185,7 @@ export default class TSGenerator extends CodeGeneratorLanguage {
* Compile the current OpenAPI definition into a TypeScript library.
*
*/
async generator() {
async compile() {
const sdkSource = this.createSourceFile();

if (Object.keys(this.schemas).length) {
Expand Down Expand Up @@ -289,7 +289,7 @@ export default class TSGenerator extends CodeGeneratorLanguage {
* Create our main SDK source file.
*
*/
createSourceFile() {
private createSourceFile() {
const { operations } = this.loadOperationsAndMethods();

const sourceFile = this.project.createSourceFile('index.ts', '');
Expand Down Expand Up @@ -450,7 +450,7 @@ sdk.server('https://eu.api.example.com/v14');`),
* infrastructure sources its data from. Without this there are no types.
*
*/
createSchemasFile() {
private createSchemasFile() {
const sourceFile = this.project.createSourceFile('schemas.ts', '');

const sortedSchemas = new Map(Array.from(Object.entries(this.schemas)).sort());
Expand Down Expand Up @@ -498,7 +498,7 @@ sdk.server('https://eu.api.example.com/v14');`),
*
* @see {@link https://npm.im/json-schema-to-ts}
*/
createTypesFile() {
private createTypesFile() {
const sourceFile = this.project.createSourceFile('types.ts', '');

sourceFile.addImportDeclarations([
Expand All @@ -513,25 +513,11 @@ sdk.server('https://eu.api.example.com/v14');`),
return sourceFile;
}

/**
* Add a new JSDoc `@tag` to an existing docblock.
*
*/
static addTagToDocblock(docblock: OptionalKind<JSDocStructure>, tag: OptionalKind<JSDocTagStructure>) {
const tags = docblock.tags ?? [];
tags.push(tag);

return {
...docblock,
tags,
};
}

/**
* Create operation accessors on the SDK.
*
*/
createOperationAccessor(
private createOperationAccessor(
operation: Operation,
operationId: string,
paramTypes?: OperationTypeHousing['types']['params'],
Expand All @@ -556,7 +542,7 @@ sdk.server('https://eu.api.example.com/v14');`),
};

if (summary && description) {
docblock = TSGenerator.addTagToDocblock(docblock, {
docblock = TSGenerator.#addTagToDocblock(docblock, {
tagName: 'summary',
text: docblockEscape(wordWrap(summary)),
});
Expand Down Expand Up @@ -609,7 +595,7 @@ sdk.server('https://eu.api.example.com/v14');`),
}

if (Number(statusPrefix) >= 4) {
docblock = TSGenerator.addTagToDocblock(docblock, {
docblock = TSGenerator.#addTagToDocblock(docblock, {
tagName: 'throws',
text: `FetchError<${status}, ${responseType}>${
responseDescription ? docblockEscape(wordWrap(` ${responseDescription}`)) : ''
Expand All @@ -626,7 +612,7 @@ sdk.server('https://eu.api.example.com/v14');`),
// 400 and 500 status code families are thrown as exceptions so adding them as a possible
// return type isn't valid.
if (Number(status) >= 400) {
docblock = TSGenerator.addTagToDocblock(docblock, {
docblock = TSGenerator.#addTagToDocblock(docblock, {
tagName: 'throws',
text: `FetchError<${status}, ${responseType}>${
responseDescription ? docblockEscape(wordWrap(` ${responseDescription}`)) : ''
Expand Down Expand Up @@ -736,7 +722,7 @@ sdk.server('https://eu.api.example.com/v14');`),
* along with every HTTP method that's in use.
*
*/
loadOperationsAndMethods() {
private loadOperationsAndMethods() {
const operations: Record</* operationId */ string, OperationTypeHousing> = {};
const methods = new Set<HttpMethods>();

Expand Down Expand Up @@ -777,7 +763,7 @@ sdk.server('https://eu.api.example.com/v14');`),
* usable TypeScript types.
*
*/
prepareParameterTypesForOperation(operation: Operation, operationId: string) {
private prepareParameterTypesForOperation(operation: Operation, operationId: string) {
const schemas = operation.getParametersAsJSONSchema({
includeDiscriminatorMappingRefs: false,
mergeIntoBodyAndMetadata: true,
Expand Down Expand Up @@ -830,7 +816,7 @@ sdk.server('https://eu.api.example.com/v14');`),
* Compile the response schemas for an API operation into usable TypeScript types.
*
*/
prepareResponseTypesForOperation(operation: Operation, operationId: string) {
private prepareResponseTypesForOperation(operation: Operation, operationId: string) {
const responseStatusCodes = operation.getResponseStatusCodes();
if (!responseStatusCodes.length) {
return undefined;
Expand Down Expand Up @@ -899,12 +885,26 @@ sdk.server('https://eu.api.example.com/v14');`),
* Add a given schema into our schema dataset that we'll be be exporting as types.
*
*/
addSchemaToExport(schema: SchemaObject, typeName: string, pointer: string) {
private addSchemaToExport(schema: SchemaObject, typeName: string, pointer: string) {
if (this.types.has(typeName)) {
return;
}

setWith(this.schemas, pointer, schema, Object);
this.types.set(typeName, `FromSchema<typeof schemas.${pointer}>`);
}

/**
* Add a new JSDoc `@tag` to an existing docblock.
*
*/
static #addTagToDocblock(docblock: OptionalKind<JSDocStructure>, tag: OptionalKind<JSDocTagStructure>) {
const tags = docblock.tags ?? [];
tags.push(tag);

return {
...docblock,
tags,
};
}
}
10 changes: 5 additions & 5 deletions packages/api/src/commands/install.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { SupportedLanguages } from '../codegen/index.js';
import type { SupportedLanguages } from '../codegen/factory.js';

import { Command, Option } from 'commander';
import figures from 'figures';
import Oas from 'oas';
import ora from 'ora';

import codegen from '../codegen/index.js';
import codegenFactory from '../codegen/factory.js';
import Fetcher from '../fetcher.js';
import promptTerminal from '../lib/prompt.js';
import logger from '../logger.js';
Expand Down Expand Up @@ -120,9 +120,9 @@ cmd

// @todo look for a prettier config and if we find one ask them if we should use it
spinner = ora('Generating your SDK').start();
const generator = codegen(language, oas, './openapi.json', identifier);
const generator = codegenFactory(language, oas, './openapi.json', identifier);
const sdkSource = await generator
.generator()
.compile()
.then(res => {
spinner.succeed(spinner.text);
return res;
Expand Down Expand Up @@ -170,7 +170,7 @@ cmd

spinner = ora('Installing required packages').start();
try {
await generator.installer(storage);
await generator.install(storage);
spinner.succeed(spinner.text);
} catch (err) {
// @todo cleanup installed files
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { TSGeneratorOptions } from '../../../src/codegen/languages/typescript.js';
import type { TSGeneratorOptions } from '../../../../src/codegen/languages/typescript/index.js';

import fs from 'node:fs/promises';
import path from 'node:path';
Expand All @@ -9,20 +9,20 @@ import Oas from 'oas';
import uniqueTempDir from 'unique-temp-dir';
import { describe, beforeEach, afterEach, it, expect, vi } from 'vitest';

import TSGenerator from '../../../src/codegen/languages/typescript.js';
import * as packageInfo from '../../../src/packageInfo.js';
import Storage from '../../../src/storage.js';
import TSGenerator from '../../../../src/codegen/languages/typescript/index.js';
import * as packageInfo from '../../../../src/packageInfo.js';
import Storage from '../../../../src/storage.js';

function assertSDKFixture(file: string, fixture: string, opts: TSGeneratorOptions = {}) {
return async () => {
const oas = await loadSpec(require.resolve(file)).then(Oas.init);
await oas.dereference({ preserveRefAsJSONSchemaTitle: true });

const ts = new TSGenerator(oas, file, fixture, opts);
const actualFiles = await ts.generator();
const actualFiles = await ts.compile();

// Determine if the generated code matches what we've got in our fixture.
const dir = path.resolve(path.join(__dirname, '..', '..', '__fixtures__', 'sdk', fixture));
const dir = path.resolve(path.join(__dirname, '..', '..', '..', '__fixtures__', 'sdk', fixture));

let expectedFiles: string[];
try {
Expand Down Expand Up @@ -62,7 +62,7 @@ function assertSDKFixture(file: string, fixture: string, opts: TSGeneratorOption
);

// Make sure that we can load the SDK without any TS compilation errors.
const sdk = await import(`../../__fixtures__/sdk/${fixture}`).then(r => r.default);
const sdk = await import(`../../../__fixtures__/sdk/${fixture}`).then(r => r.default);
expect(sdk.constructor.name).toBe('SDK');
};
}
Expand All @@ -88,7 +88,7 @@ describe('typescript', () => {
await storage.load();

const ts = new TSGenerator(oas, './openapi.json', 'petstore', { compilerTarget: 'cjs' });
await ts.installer(storage, { logger, dryRun: true });
await ts.install(storage, { logger, dryRun: true });

const pkgJson = await fs
.readFile(path.join(storage.getIdentifierStorageDir(), 'package.json'), 'utf-8')
Expand Down Expand Up @@ -171,7 +171,7 @@ describe('typescript', () => {
});

it('should be able to make an API request (TS)', async () => {
const sdk = await import('../../__fixtures__/sdk/simple-ts/index.js').then(r => r.default);
const sdk = await import('../../../__fixtures__/sdk/simple-ts/index.js').then(r => r.default);
fetchMock.get('http://petstore.swagger.io/v2/pet/findByStatus?status=available', mockResponse.searchParams);

await sdk.findPetsByStatus({ status: ['available'] }).then(({ data, status, headers, res }) => {
Expand All @@ -183,7 +183,7 @@ describe('typescript', () => {
});

it('should be able to make an API request with an `accept` header`', async () => {
const sdk = await import('../../__fixtures__/sdk/simple-ts/index.js').then(r => r.default);
const sdk = await import('../../../__fixtures__/sdk/simple-ts/index.js').then(r => r.default);
fetchMock.get('http://petstore.swagger.io/v2/pet/findByStatus?status=available', mockResponse.headers);

await sdk
Expand All @@ -203,20 +203,20 @@ describe('typescript', () => {
*
* @see {@link https://github.com/readmeio/api/pull/734}
*/
it.skip('should be able to make an API request (JS + CommonJS)', async () => {
const sdk = await import('../../__fixtures__/sdk/simple-js-cjs/index.js').then(r => r.default);
fetchMock.get('http://petstore.swagger.io/v2/pet/findByStatus?status=available', mockResponse.searchParams);

await sdk.findPetsByStatus({ status: ['available'] }).then(({ data, status, headers, res }) => {
expect(data).toBe('/v2/pet/findByStatus?status=available');
expect(status).toBe(200);
expect(headers.constructor.name).toBe('Headers');
expect(res.constructor.name).toBe('Response');
});
});
// it.skip('should be able to make an API request (JS + CommonJS)', async () => {
// const sdk = await import('../../../__fixtures__/sdk/simple-js-cjs/index.js').then(r => r.default);
// fetchMock.get('http://petstore.swagger.io/v2/pet/findByStatus?status=available', mockResponse.searchParams);

// await sdk.findPetsByStatus({ status: ['available'] }).then(({ data, status, headers, res }) => {
// expect(data).toBe('/v2/pet/findByStatus?status=available');
// expect(status).toBe(200);
// expect(headers.constructor.name).toBe('Headers');
// expect(res.constructor.name).toBe('Response');
// });
// });

it('should be able to make an API request (JS + ESM)', async () => {
const sdk = await import('../../__fixtures__/sdk/simple-js-esm/index.js').then(r => r.default);
const sdk = await import('../../../__fixtures__/sdk/simple-js-esm/index.js').then(r => r.default);
fetchMock.get('http://petstore.swagger.io/v2/pet/findByStatus?status=available', mockResponse.searchParams);

await sdk.findPetsByStatus({ status: ['available'] }).then(({ data, status, headers, res }) => {
Expand All @@ -240,7 +240,7 @@ describe('typescript', () => {
});

const ts = new TSGenerator(oas, 'no-paths', './no-paths.json');
await expect(ts.generator()).rejects.toThrow(
await expect(ts.compile()).rejects.toThrow(
'Sorry, this OpenAPI definition does not have any operation paths to generate an SDK for.',
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import Oas from 'oas';
import OASNormalize from 'oas-normalize';
import { describe, it, expect } from 'vitest';

import TSGenerator from '../../../../src/codegen/languages/typescript.js';
import TSGenerator from '../../../../src/codegen/languages/typescript/index.js';

// These APIs don't have any schemas so they should only be generating an `index.ts`.
const APIS_WITHOUT_SCHEMAS = ['poemist.com'];
Expand Down Expand Up @@ -70,7 +70,7 @@ describe('typescript smoketest', () => {
await oas.dereference({ preserveRefAsJSONSchemaTitle: true });

const ts = new TSGenerator(oas, './path/not/needed.json', name);
const res = await ts.generator();
const res = await ts.compile();

if (APIS_WITHOUT_SCHEMAS.includes(name)) {
expect(Object.keys(res)).toStrictEqual(['index.ts']);
Expand Down

0 comments on commit 518736d

Please sign in to comment.