Skip to content

Commit

Permalink
Merge pull request #107 from jordanshatford/feat/unify-v2-v3-parsing
Browse files Browse the repository at this point in the history
feat: unify v2 v3 parsing
  • Loading branch information
mrlubos authored Mar 21, 2024
2 parents 5d8c143 + 34b3ddb commit a848dba
Show file tree
Hide file tree
Showing 31 changed files with 253 additions and 250 deletions.
102 changes: 2 additions & 100 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { describe, it } from 'vitest';

import { createClient, parseOpenApiSpecification } from './index';
import * as parseV2 from './openApi/v2';
import * as parseV3 from './openApi/v3';
import { createClient } from './index';

describe('index', () => {
it('parses v2 without issues', async () => {
Expand Down Expand Up @@ -37,99 +35,3 @@ describe('index', () => {
});
});
});

describe('parseOpenApiSpecification', () => {
afterEach(() => {
vi.restoreAllMocks();
});

const options: Parameters<typeof parseOpenApiSpecification>[1] = {
client: 'fetch',
enums: true,
exportCore: true,
exportModels: true,
exportSchemas: true,
exportServices: true,
format: true,
input: '',
lint: false,
operationId: true,
output: '',
postfixModels: '',
postfixServices: '',
serviceResponse: 'body',
useDateType: false,
useOptions: true,
write: false,
};

it('uses v2 parser', () => {
const spy = vi.spyOn(parseV2, 'parse');

const spec: Parameters<typeof parseOpenApiSpecification>[0] = {
info: {
title: 'dummy',
version: '1.0',
},
paths: {},
swagger: '2',
};
parseOpenApiSpecification(spec, options);
expect(spy).toHaveBeenCalledWith(spec, options);

const spec2: Parameters<typeof parseOpenApiSpecification>[0] = {
info: {
title: 'dummy',
version: '1.0',
},
paths: {},
swagger: '2.0',
};
parseOpenApiSpecification(spec2, options);
expect(spy).toHaveBeenCalledWith(spec2, options);
});

it('uses v3 parser', () => {
const spy = vi.spyOn(parseV3, 'parse');

const spec: Parameters<typeof parseOpenApiSpecification>[0] = {
info: {
title: 'dummy',
version: '1.0',
},
openapi: '3',
paths: {},
};
parseOpenApiSpecification(spec, options);
expect(spy).toHaveBeenCalledWith(spec, options);

const spec2: Parameters<typeof parseOpenApiSpecification>[0] = {
info: {
title: 'dummy',
version: '1.0',
},
openapi: '3.0',
paths: {},
};
parseOpenApiSpecification(spec2, options);
expect(spy).toHaveBeenCalledWith(spec2, options);

const spec3: Parameters<typeof parseOpenApiSpecification>[0] = {
info: {
title: 'dummy',
version: '1.0',
},
openapi: '3.1.0',
paths: {},
};
parseOpenApiSpecification(spec3, options);
expect(spy).toHaveBeenCalledWith(spec3, options);
});

it('throws on unknown version', () => {
// @ts-ignore
expect(() => parseOpenApiSpecification({ foo: 'bar' }, options)).toThrow(
`Unsupported Open API specification: ${JSON.stringify({ foo: 'bar' }, null, 2)}`
);
});
});
15 changes: 2 additions & 13 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import { pathToFileURL } from 'node:url';

import { sync } from 'cross-spawn';

import { parse as parseV2 } from './openApi/v2';
import { parse as parseV3 } from './openApi/v3';
import { parse } from './openApi';
import type { Client } from './types/client';
import type { Config, UserConfig } from './types/config';
import { getOpenApiSpec } from './utils/getOpenApiSpec';
Expand All @@ -19,16 +18,6 @@ type Dependencies = Record<string, unknown>;
// TODO: add support for `openapi-ts.config.ts`
const configFiles = ['openapi-ts.config.js', 'openapi-ts.config.cjs', 'openapi-ts.config.mjs'];

export const parseOpenApiSpecification = (openApi: Awaited<ReturnType<typeof getOpenApiSpec>>, config: Config) => {
if ('openapi' in openApi) {
return parseV3(openApi, config);
}
if ('swagger' in openApi) {
return parseV2(openApi, config);
}
throw new Error(`Unsupported Open API specification: ${JSON.stringify(openApi, null, 2)}`);
};

const processOutput = (config: Config, dependencies: Dependencies) => {
if (config.format) {
if (dependencies.prettier) {
Expand Down Expand Up @@ -174,7 +163,7 @@ export async function createClient(userConfig: UserConfig): Promise<Client> {
? await getOpenApiSpec(config.input)
: (config.input as unknown as Awaited<ReturnType<typeof getOpenApiSpec>>);

const client = postProcessClient(parseOpenApiSpecification(openApi, config));
const client = postProcessClient(parse(openApi, config));
const templates = registerHandlebarTemplates(config, client);

if (config.write) {
Expand Down
101 changes: 101 additions & 0 deletions src/openApi/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { afterEach, describe, expect, it, vi } from 'vitest';

import { parse } from '..';
import * as parseV2 from '../v2';
import * as parseV3 from '../v3';

describe('parse', () => {
afterEach(() => {
vi.restoreAllMocks();
});

const options: Parameters<typeof parse>[1] = {
client: 'fetch',
enums: true,
exportCore: true,
exportModels: true,
exportSchemas: true,
exportServices: true,
format: true,
input: '',
lint: false,
operationId: true,
output: '',
postfixModels: '',
postfixServices: '',
serviceResponse: 'body',
useDateType: false,
useOptions: true,
write: false,
};

it('uses v2 parser', () => {
const spy = vi.spyOn(parseV2, 'parse');

const spec: Parameters<typeof parse>[0] = {
info: {
title: 'dummy',
version: '1.0',
},
paths: {},
swagger: '2',
};
parse(spec, options);
expect(spy).toHaveBeenCalledWith(spec, options);

const spec2: Parameters<typeof parse>[0] = {
info: {
title: 'dummy',
version: '1.0',
},
paths: {},
swagger: '2.0',
};
parse(spec2, options);
expect(spy).toHaveBeenCalledWith(spec2, options);
});

it('uses v3 parser', () => {
const spy = vi.spyOn(parseV3, 'parse');

const spec: Parameters<typeof parse>[0] = {
info: {
title: 'dummy',
version: '1.0',
},
openapi: '3',
paths: {},
};
parse(spec, options);
expect(spy).toHaveBeenCalledWith(spec, options);

const spec2: Parameters<typeof parse>[0] = {
info: {
title: 'dummy',
version: '1.0',
},
openapi: '3.0',
paths: {},
};
parse(spec2, options);
expect(spy).toHaveBeenCalledWith(spec2, options);

const spec3: Parameters<typeof parse>[0] = {
info: {
title: 'dummy',
version: '1.0',
},
openapi: '3.1.0',
paths: {},
};
parse(spec3, options);
expect(spy).toHaveBeenCalledWith(spec3, options);
});

it('throws on unknown version', () => {
// @ts-ignore
expect(() => parse({ foo: 'bar' }, options)).toThrow(
`Unsupported Open API specification: ${JSON.stringify({ foo: 'bar' }, null, 2)}`
);
});
});
4 changes: 4 additions & 0 deletions src/openApi/common/interfaces/OpenApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { OpenApi as OpenApiV2 } from '../../v2/interfaces/OpenApi';
import type { OpenApi as OpenApiV3 } from '../../v3/interfaces/OpenApi';

export type OpenApi = OpenApiV2 | OpenApiV3;
Original file line number Diff line number Diff line change
@@ -1,8 +1,40 @@
import { describe, expect, it } from 'vitest';

import { getRef } from './getRef';
import { getRef } from '../getRef';

describe('getRef', () => {
describe('getRef (v2)', () => {
it('should produce correct result', () => {
expect(
getRef(
{
swagger: '2.0',
info: {
title: 'dummy',
version: '1.0',
},
host: 'localhost:8080',
basePath: '/api',
schemes: ['http', 'https'],
paths: {},
definitions: {
Example: {
description: 'This is an Example model ',
type: 'integer',
},
},
},
{
$ref: '#/definitions/Example',
}
)
).toEqual({
description: 'This is an Example model ',
type: 'integer',
});
});
});

describe('getRef (v3)', () => {
it('should produce correct result', () => {
expect(
getRef(
Expand Down
13 changes: 13 additions & 0 deletions src/openApi/common/parser/__tests__/service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { describe, expect, it } from 'vitest';

import { getServiceVersion } from '../service';

describe('getServiceVersion', () => {
it.each([
{ input: '1.0', expected: '1.0' },
{ input: 'v1.2', expected: '1.2' },
{ input: 'V2.4', expected: '2.4' },
])('should get $expected when version is $input', ({ input, expected }) => {
expect(getServiceVersion(input)).toEqual(expected);
});
});
34 changes: 34 additions & 0 deletions src/openApi/common/parser/__tests__/sort.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, it } from 'vitest';

import { toSortedByRequired } from '../sort';

describe('sort', () => {
it.each([
{
input: [
{ id: 'test', isRequired: false },
{ id: 'test2', isRequired: true },
{ id: 'test3', isRequired: true },
],
expected: [
{ id: 'test2', isRequired: true },
{ id: 'test3', isRequired: true },
{ id: 'test', isRequired: false },
],
},
{
input: [
{ id: 'test', isRequired: false },
{ id: 'test2', isRequired: false },
{ id: 'test3', isRequired: true, default: 'something' },
],
expected: [
{ id: 'test', isRequired: false },
{ id: 'test2', isRequired: false },
{ id: 'test3', isRequired: true, default: 'something' },
],
},
])('should sort $input by required to produce $expected', ({ input, expected }) => {
expect(toSortedByRequired(input)).toEqual(expected);
});
});
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { OpenApi } from '../interfaces/OpenApi';
import type { OpenApiReference } from '../interfaces/OpenApiReference';
import type { OpenApiReference as OpenApiReferenceV2 } from '../../v2/interfaces/OpenApiReference';
import type { OpenApiReference as OpenApiReferenceV3 } from '../../v3/interfaces/OpenApiReference';
import { OpenApi } from '../interfaces/OpenApi';

const ESCAPED_REF_SLASH = /~1/g;
const ESCAPED_REF_TILDE = /~0/g;

export const getRef = <T>(openApi: OpenApi, item: T & OpenApiReference): T => {
export function getRef<T>(openApi: OpenApi, item: T & (OpenApiReferenceV2 | OpenApiReferenceV3)): T {
if (item.$ref) {
// Fetch the paths to the definitions, this converts:
// "#/components/schemas/Form" to ["components", "schemas", "Form"]
Expand All @@ -30,4 +31,4 @@ export const getRef = <T>(openApi: OpenApi, item: T & OpenApiReference): T => {
return result as T;
}
return item as T;
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@
* This basically removes any "v" prefix from the version string.
* @param version
*/
export const getServiceVersion = (version = '1.0'): string => String(version).replace(/^v/gi, '');
export function getServiceVersion(version = '1.0'): string {
return String(version).replace(/^v/gi, '');
}
13 changes: 13 additions & 0 deletions src/openApi/common/parser/sort.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Sort list of values and ensure that required parameters are first so that we do not generate
* invalid types. Optional parameters cannot be positioned after required ones.
*/
export function toSortedByRequired<T extends { isRequired: boolean; default?: string }>(values: T[]): T[] {
return values.sort((a, b) => {
const aNeedsValue = a.isRequired && a.default === undefined;
const bNeedsValue = b.isRequired && b.default === undefined;
if (aNeedsValue && !bNeedsValue) return -1;
if (bNeedsValue && !aNeedsValue) return 1;
return 0;
});
}
Loading

0 comments on commit a848dba

Please sign in to comment.