Skip to content

Commit

Permalink
refactor: introduce stricter OpenApiDocument types (#261)
Browse files Browse the repository at this point in the history
  • Loading branch information
toomuchdesign committed May 22, 2024
1 parent f76c10d commit cfb2e41
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 60 deletions.
23 changes: 13 additions & 10 deletions src/openapiToTsJsonSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
clearFolder,
makeTsJsonSchemaFiles,
SCHEMA_ID_SYMBOL,
convertOpenApiToJsonSchema,
convertOpenApiDocumentToJsonSchema,
convertOpenApiPathsParameters,
addSchemaToMetaData,
makeId,
Expand All @@ -18,6 +18,7 @@ import {
import type {
SchemaMetaDataMap,
OpenApiObject,
OpenApiDocument,
JSONSchema,
ReturnPayload,
Options,
Expand Down Expand Up @@ -78,19 +79,23 @@ export async function openapiToTsJsonSchema(
const jsonSchemaParser = new $RefParser();

// Resolve and inline external $ref definitions
const bundledOpenApiSchema = await openApiParser.bundle(openApiSchemaPath);
// Convert oas definitions to JSON schema
const initialJsonSchema = convertOpenApiToJsonSchema(bundledOpenApiSchema);
// @ts-expect-error @apidevtools/json-schema-ref-parser types supports JSON schemas only
const bundledOpenApiSchema: OpenApiDocument =
await openApiParser.bundle(openApiSchemaPath);

// Convert oas definitions to JSON schema (excluding paths and parameter objects)
const initialJsonSchema =
convertOpenApiDocumentToJsonSchema(bundledOpenApiSchema);

const inlinedRefs: Map<
string,
{ openApiDefinition: OpenApiObject; jsonSchema: JSONSchema }
> = new Map();

// Inline and collect internal $ref definitions
const dereferencedJsonSchema = await jsonSchemaParser.dereference(
initialJsonSchema,
{
// @ts-expect-error @apidevtools/json-schema-ref-parser types supports JSON schemas only
const dereferencedJsonSchema: OpenApiDocument =
await jsonSchemaParser.dereference(initialJsonSchema, {
dereference: {
// @ts-expect-error onDereference seems not to be properly typed
onDereference: (ref, inlinedSchema) => {
Expand Down Expand Up @@ -130,8 +135,7 @@ export async function openapiToTsJsonSchema(
}
},
},
},
);
});

const jsonSchema = convertOpenApiPathsParameters(dereferencedJsonSchema);
const schemaMetaDataMap: SchemaMetaDataMap = new Map();
Expand Down Expand Up @@ -163,7 +167,6 @@ export async function openapiToTsJsonSchema(
const openApiDefinitions = get(bundledOpenApiSchema, definitionPath);

for (const schemaName in jsonSchemaDefinitions) {
// Create expected OpenAPI ref
const id = makeId({
schemaRelativeDirName: definitionPath,
schemaName,
Expand Down
8 changes: 7 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,21 @@ import type {
SchemaObject as SchemaObject_v3_0,
ParameterObject as ParameterObject_v3_0,
ReferenceObject as ReferenceObject_v3_0,
OpenAPIObject as OpenAPIObject_v3_0,
} from 'openapi3-ts/oas30';
import type {
PathItemObject as PathItemObject_v3_1,
SchemaObject as SchemaObject_v3_1,
ParameterObject as ParameterObject_v3_1,
ReferenceObject as ReferenceObject_v3_1,
OpenAPIObject as OpenAPIObject_v3_1,
} from 'openapi3-ts/oas31';

export type OpenApiDocument = Record<string, any>;
export type OpenApiDocument = Omit<
OpenAPIObject_v3_0 | OpenAPIObject_v3_1,
'openapi' | 'info'
>;

// This type should represent any generated OpenAPI
type OpenApiObject_v3_0 =
| PathItemObject_v3_0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ function convertToJsonSchema<Value extends unknown>(
*
* @TODO Find a nicer way to convert convert all the expected OpenAPI schemas
*/
export function convertOpenApiToJsonSchema(
export function convertOpenApiDocumentToJsonSchema(
schema: OpenApiDocument,
): Record<string, unknown> {
): OpenApiDocument {
return mapObject(
schema,
(key, value) => {
Expand Down
2 changes: 1 addition & 1 deletion src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export { patchJsonSchema } from './makeTsJsonSchema/patchJsonSchema';
export { makeTsJsonSchema } from './makeTsJsonSchema';
export { convertOpenApiPathsParameters } from './convertOpenApiPathsParameters';
export { convertOpenApiToJsonSchema } from './convertOpenApiToJsonSchema';
export { convertOpenApiDocumentToJsonSchema } from './convertOpenApiDocumentToJsonSchema';
export { convertOpenApiParameterToJsonSchema } from './convertOpenApiParameterToJsonSchema';
export { makeTsJsonSchemaFiles } from './makeTsJsonSchemaFiles';
export { parseId } from './parseId';
Expand Down
120 changes: 74 additions & 46 deletions test/unit-tests/convertOpenApiToJsonSchema.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { describe, it, expect, vi } from 'vitest';
import { convertOpenApiToJsonSchema } from '../../src/utils';
import { convertOpenApiDocumentToJsonSchema } from '../../src/utils';
import * as openapiSchemaToJsonSchema from '@openapi-contrib/openapi-schema-to-json-schema';

const openApiDefinition = {
type: 'string',
type: 'string' as const,
nullable: true,
enum: ['yes', 'no'],
};
Expand All @@ -13,31 +13,33 @@ const jsonSchemaDefinition = {
enum: ['yes', 'no', null],
};

describe('convertOpenApiToJsonSchema', () => {
describe('convertOpenApiDocumentToJsonSchema', () => {
describe('Nested definitions', () => {
it('convert nested definitions', () => {
const actual = convertOpenApiToJsonSchema({
foo: { bar: openApiDefinition },
const actual = convertOpenApiDocumentToJsonSchema({
components: { schemas: { bar: openApiDefinition } },
});
const expected = {
foo: { bar: jsonSchemaDefinition },
components: { schemas: { bar: jsonSchemaDefinition } },
};
expect(actual).toEqual(expected);
});
});

describe('array of definitions', () => {
it('convert nested definitions', () => {
const actual = convertOpenApiToJsonSchema({
foo: { schema: { oneOf: [openApiDefinition, openApiDefinition] } },
const actual = convertOpenApiDocumentToJsonSchema({
components: {
schemas: { bar: { oneOf: [openApiDefinition, openApiDefinition] } },
},
});

const expected = {
foo: {
schema: { oneOf: [jsonSchemaDefinition, jsonSchemaDefinition] },
components: {
schemas: {
bar: { oneOf: [jsonSchemaDefinition, jsonSchemaDefinition] },
},
},
};

expect(actual).toEqual(expected);
});
});
Expand All @@ -50,7 +52,7 @@ describe('convertOpenApiToJsonSchema', () => {
type: ['string'],
},
};
const actual = convertOpenApiToJsonSchema(definition);
const actual = convertOpenApiDocumentToJsonSchema(definition);
expect(actual).toEqual(definition);
});
});
Expand All @@ -61,7 +63,7 @@ describe('convertOpenApiToJsonSchema', () => {
in: 'path',
name: 'userId',
};
const actual = convertOpenApiToJsonSchema(definition);
const actual = convertOpenApiDocumentToJsonSchema(definition);
expect(actual).toEqual(definition);
});
});
Expand All @@ -80,7 +82,7 @@ describe('convertOpenApiToJsonSchema', () => {
},
],
};
const actual = convertOpenApiToJsonSchema(definition);
const actual = convertOpenApiDocumentToJsonSchema(definition);
expect(actual).toEqual(definition);
});
});
Expand All @@ -91,23 +93,27 @@ describe('convertOpenApiToJsonSchema', () => {
type: 'http',
scheme: 'bearer',
};
const actual = convertOpenApiToJsonSchema(definition);
const actual = convertOpenApiDocumentToJsonSchema(definition);
expect(actual).toEqual(definition);
});
});

describe('OpenAPI definition as object prop (entities converted multiple times)', () => {
it('convert nested definitions', () => {
const actual = convertOpenApiToJsonSchema({
schemaName: {
type: 'object',
properties: {
two: {
const actual = convertOpenApiDocumentToJsonSchema({
components: {
schemas: {
schemaName: {
type: 'object',
properties: {
three: {
type: 'string',
nullable: true,
two: {
type: 'object',
properties: {
three: {
type: 'string',
nullable: true,
},
},
},
},
},
Expand All @@ -116,18 +122,22 @@ describe('convertOpenApiToJsonSchema', () => {
});

const expected = {
schemaName: {
properties: {
two: {
components: {
schemas: {
schemaName: {
properties: {
three: {
type: ['string', 'null'],
two: {
properties: {
three: {
type: ['string', 'null'],
},
},
type: 'object',
},
},
type: 'object',
},
},
type: 'object',
},
};
expect(actual).toEqual(expected);
Expand All @@ -136,24 +146,35 @@ describe('convertOpenApiToJsonSchema', () => {

describe('Object with "type" prop (#211)', () => {
it('convert object definitions', () => {
const actual = convertOpenApiToJsonSchema({
type: 'object',
properties: {
type: { type: 'string', nullable: true },
bar: { type: 'string' },
const actual = convertOpenApiDocumentToJsonSchema({
components: {
schemas: {
foo: {
type: 'object',
properties: {
type: { type: 'string', nullable: true },
bar: { type: 'string' },
},
required: ['type', 'bar'],
},
},
},
required: ['type', 'bar'],
});

const expected = {
type: 'object',
properties: {
type: { type: ['string', 'null'] },
bar: { type: 'string' },
components: {
schemas: {
foo: {
type: 'object',
properties: {
type: { type: ['string', 'null'] },
bar: { type: 'string' },
},
required: ['type', 'bar'],
},
},
},
required: ['type', 'bar'],
};

expect(actual).toEqual(expected);
});
});
Expand All @@ -166,10 +187,17 @@ describe('convertOpenApiToJsonSchema', () => {
});

expect(() => {
convertOpenApiToJsonSchema({
type: 'object',
properties: {
bar: { type: 'invalid-type' },
convertOpenApiDocumentToJsonSchema({
components: {
schemas: {
foo: {
type: 'object',
properties: {
// @ts-expect-error Deliberately testing invalid definition types
bar: { type: 'invalid-type' },
},
},
},
},
});
}).toThrowError(
Expand Down

0 comments on commit cfb2e41

Please sign in to comment.