Skip to content

Commit

Permalink
refactor: schema generation to be less coupled to comment-json (#262)
Browse files Browse the repository at this point in the history
* feat: add shouldBeGenerated to metaData

* refactor: remove recursion comments

* refactor: generate metaData for all  schemas for all refHandling modes

* refactor: abstract stringify function

* chore: add changeset
  • Loading branch information
toomuchdesign committed May 25, 2024
1 parent e6f83b0 commit e63f47e
Show file tree
Hide file tree
Showing 15 changed files with 127 additions and 87 deletions.
5 changes: 5 additions & 0 deletions .changeset/healthy-panthers-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openapi-ts-json-schema': minor
---

`shouldBeGenerated` prop added to `metaData`
5 changes: 5 additions & 0 deletions .changeset/mighty-pans-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openapi-ts-json-schema': minor
---

Recursion comments removed
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ Beside generating the expected schema files under `outputPath`, `openapiToTsJson
// Original dereferenced JSON schema
isRef: boolean;
// True if schemas is used as a `$ref`
shouldBeGenerated: boolean;
// True is the schema has to be generated

absoluteDirName: string;
// Absolute path pointing to schema folder (posix or win32). Eg: `"Users/username/output/path/components/schemas"`
Expand Down
47 changes: 28 additions & 19 deletions src/openapiToTsJsonSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,25 +140,6 @@ export async function openapiToTsJsonSchema(
const jsonSchema = convertOpenApiPathsParameters(dereferencedJsonSchema);
const schemaMetaDataMap: SchemaMetaDataMap = new Map();

/**
* Create meta data for $ref schemas which have been previously dereferenced.
* It happens only with "import" and "keep" refHandling since they expect
* $ref schemas to be generated no matter of
*/
if (refHandling === 'import' || refHandling === 'keep') {
for (const [id, { openApiDefinition, jsonSchema }] of inlinedRefs) {
addSchemaToMetaData({
id,
$id: $idMapper({ id }),
schemaMetaDataMap,
openApiDefinition,
jsonSchema,
outputPath,
isRef: true,
});
}
}

/**
* Create meta data for each output schema
*/
Expand All @@ -180,10 +161,38 @@ export async function openapiToTsJsonSchema(
jsonSchema: jsonSchemaDefinitions[schemaName],
outputPath,
isRef: inlinedRefs.has(id),
shouldBeGenerated: true,
});
}
}

/**
* Create meta data for each $ref schemas which have been previously dereferenced.
*/
for (const [id, { openApiDefinition, jsonSchema }] of inlinedRefs) {
/**
* In "inline" mode $ref schemas not explicitly marked for generation
* should not be generated
*
* All the other "refHandling" modes generate all $ref schemas
*/
let shouldBeGenerated = true;
if (refHandling === 'inline' && !schemaMetaDataMap.has(id)) {
shouldBeGenerated = false;
}

addSchemaToMetaData({
id,
$id: $idMapper({ id }),
schemaMetaDataMap,
openApiDefinition,
jsonSchema,
outputPath,
isRef: true,
shouldBeGenerated,
});
}

const returnPayload: ReturnPayload = {
outputPath,
metaData: { schemas: schemaMetaDataMap },
Expand Down
3 changes: 2 additions & 1 deletion src/plugins/fastifyIntegrationPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ const fastifyIntegrationPlugin: Plugin<PluginOptions | void> = ({
onBeforeGeneration: async ({ outputPath, metaData, options, utils }) => {
// Derive the schema data necessary to generate the declarations
const allSchemas = [...metaData.schemas]
.map(([id, schema]) => schema)
.map(([_id, schema]) => schema)
.filter((schema) => schema.shouldBeGenerated)
.map(({ absoluteImportPath, uniqueName, id, isRef }) => {
return {
importPath: utils.makeRelativeModulePath({
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export type Options = {
* @property `id` - Internal unique schema identifier. Eg `"/components/schemas/MySchema"`
* @property `$id` - JSON schema Compound Schema Document `$id`. Eg `"/components/schemas/MySchema"`
* @property `isRef` - True if schemas is used as `$ref`
* @property `shouldBeGenerated` - True is the schema has to be generated
* @property `uniqueName` - Unique JavaScript identifier used as import name. Eg: `"componentsSchemasMySchema"`
* @property `openApiDefinition` - Original dereferenced openAPI definition
* @property `originalSchema` - Original dereferenced JSON schema
Expand All @@ -77,6 +78,7 @@ export type SchemaMetaData = {
id: string;
$id: string;
isRef: boolean;
shouldBeGenerated: boolean;
uniqueName: string;
openApiDefinition?: OpenApiObject;
originalSchema: JSONSchema;
Expand Down
46 changes: 23 additions & 23 deletions src/utils/addSchemaToMetaData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export function addSchemaToMetaData({
openApiDefinition,
jsonSchema,
isRef,
shouldBeGenerated,
// Options
outputPath,
}: {
Expand All @@ -33,33 +34,32 @@ export function addSchemaToMetaData({
openApiDefinition: OpenApiObject;
jsonSchema: JSONSchema;
isRef: boolean;
shouldBeGenerated: boolean;
outputPath: string;
}): void {
// Do not override existing meta info of inlined schemas
if (!schemaMetaDataMap.has(id)) {
const { schemaRelativeDirName, schemaName } = parseId(id);
const absoluteDirName = path.join(outputPath, schemaRelativeDirName);
const schemaFileName = filenamify(schemaName);
const absoluteImportPath = path.join(absoluteDirName, schemaFileName);
const { schemaRelativeDirName, schemaName } = parseId(id);
const absoluteDirName = path.join(outputPath, schemaRelativeDirName);
const schemaFileName = filenamify(schemaName);
const absoluteImportPath = path.join(absoluteDirName, schemaFileName);

// Convert components.parameters after convertOpenApiPathsParameters is called
if (isOpenApiParameterObject(openApiDefinition)) {
jsonSchema = convertOpenApiParameterToJsonSchema(openApiDefinition);
}
// Convert components.parameters after convertOpenApiPathsParameters is called
if (isOpenApiParameterObject(openApiDefinition)) {
jsonSchema = convertOpenApiParameterToJsonSchema(openApiDefinition);
}

const metaInfo: SchemaMetaData = {
id,
$id,
uniqueName: namify(id),
isRef,
openApiDefinition,
originalSchema: jsonSchema,
const metaInfo: SchemaMetaData = {
id,
$id,
uniqueName: namify(id),
isRef,
shouldBeGenerated,
openApiDefinition,
originalSchema: jsonSchema,

absoluteDirName,
absoluteImportPath,
absolutePath: absoluteImportPath + '.ts',
};
absoluteDirName,
absoluteImportPath,
absolutePath: absoluteImportPath + '.ts',
};

schemaMetaDataMap.set(id, metaInfo);
}
schemaMetaDataMap.set(id, metaInfo);
}
9 changes: 2 additions & 7 deletions src/utils/makeTsJsonSchema/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { stringify } from 'comment-json';
import { stringify } from './stringify';
import { replaceInlinedRefsWithStringPlaceholder } from './replaceInlinedRefsWithStringPlaceholder';
import { replacePlaceholdersWithImportedSchemas } from './replacePlaceholdersWithImportedSchemas';
import { replacePlaceholdersWithRefs } from './replacePlaceholdersWithRefs';
import { makeCircularRefReplacer } from './makeCircularRefReplacer';
import { patchJsonSchema } from './patchJsonSchema';
import { formatTypeScript } from '../';
import type {
Expand Down Expand Up @@ -48,11 +47,7 @@ export async function makeTsJsonSchema({
* Stringifying schema with "comment-json" instead of JSON.stringify
* to generate inline comments for "inline" refHandling
*/
const stringifiedSchema = stringify(
patchedSchema,
makeCircularRefReplacer(),
2,
);
const stringifiedSchema = stringify(patchedSchema);

let tsSchema = `
const schema = ${stringifiedSchema} as const;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { getId } from './getId';
import { stringify as commentJsonStringify } from 'comment-json';

/**
* JSON.stringify replacer
* Replace circular references with {}
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value#circular_references
*/
export function makeCircularRefReplacer(): (
key: string,
value: unknown,
) => unknown {
function makeCircularRefReplacer(): (key: string, value: unknown) => unknown {
const ancestors: unknown[] = [];
return function (this: unknown, key: string, value: unknown) {
if (typeof value !== 'object' || value === null) {
Expand All @@ -23,19 +20,16 @@ export function makeCircularRefReplacer(): (

// @NOTE Should we make recursion depth configurable?
if (ancestors.includes(value)) {
const id = getId(value);
return {
// Drop an inline comment about recursion interruption
[Symbol.for('before')]: [
{
type: 'LineComment',
value: ` Circular recursion interrupted. Schema id: "${id}"`,
},
],
};
return {};
}

ancestors.push(value);
return value;
};
}

const circularReplacer = makeCircularRefReplacer();

export function stringify(input: unknown): string {
return commentJsonStringify(input, circularReplacer, 2);
}
20 changes: 11 additions & 9 deletions src/utils/makeTsJsonSchemaFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,17 @@ export async function makeTsJsonSchemaFiles({
$idMapper: $idMapper;
}) {
for (const [_, metaData] of schemaMetaDataMap) {
const tsSchema = await makeTsJsonSchema({
metaData,
schemaMetaDataMap,
refHandling,
schemaPatcher,
$idMapper,
});
if (metaData.shouldBeGenerated) {
const tsSchema = await makeTsJsonSchema({
metaData,
schemaMetaDataMap,
refHandling,
schemaPatcher,
$idMapper,
});

const { absolutePath } = metaData;
await saveFile({ path: [absolutePath], data: tsSchema });
const { absolutePath } = metaData;
await saveFile({ path: [absolutePath], data: tsSchema });
}
}
}
33 changes: 27 additions & 6 deletions test/circularReference.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,33 @@ describe('Circular reference', () => {
description: 'February description',
type: 'object',
properties: {
previousMonth: {},
previousMonth: {
description: 'January description',
properties: {},
type: 'object',
},
},
},
nextMonthTwo: {
description: 'February description',
type: 'object',
properties: {
previousMonth: {},
previousMonth: {
description: 'January description',
properties: {},
type: 'object',
},
},
},
nextMonthThree: {
description: 'February description',
type: 'object',
properties: {
previousMonth: {},
previousMonth: {
description: 'January description',
properties: {},
type: 'object',
},
},
},
},
Expand All @@ -67,13 +79,22 @@ describe('Circular reference', () => {
type: "object",
properties: {
nextMonth: {
// Circular recursion interrupted. Schema id: "/components/schemas/February"
// $ref: "#/components/schemas/February"
description: "February description",
type: "object",
properties: {},
},
nextMonthTwo: {
// Circular recursion interrupted. Schema id: "/components/schemas/February"
// $ref: "#/components/schemas/February"
description: "February description",
type: "object",
properties: {},
},
nextMonthThree: {
// Circular recursion interrupted. Schema id: "/components/schemas/February"
// $ref: "#/components/schemas/February"
description: "February description",
type: "object",
properties: {},
},
},
},`;
Expand Down
2 changes: 2 additions & 0 deletions test/metaData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ describe('Returned "metaData"', async () => {
type: ['string', 'null'],
},
isRef: true,
shouldBeGenerated: true,

absoluteDirName: `${outputPath}/components/schemas`.replaceAll(
'/',
Expand Down Expand Up @@ -97,6 +98,7 @@ describe('Returned "metaData"', async () => {
type: 'object',
},
isRef: false,
shouldBeGenerated: true,

absoluteDirName: `${outputPath}/components/schemas`.replaceAll(
'/',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,25 @@ describe('fastifyIntegration plugin', () => {
// @TODO find a better way to assert against generated types
const expectedAsText = await formatTypeScript(`
// File autogenerated by "openapi-ts-json-schema". Do not edit :)
import { with$id as componentsSchemasAnswer } from "./components/schemas/Answer";
import { with$id as componentsSchemasJanuary } from "./components/schemas/January";
import { with$id as componentsSchemasFebruary } from "./components/schemas/February";
import { with$id as componentsSchemasMarch } from "./components/schemas/March";
import { with$id as componentsSchemasAnswer } from "./components/schemas/Answer";
// RefSchemas type: tuple of $ref schema types to enable json-schema-to-ts hydrate $refs via "references" option
export type RefSchemas = [
typeof componentsSchemasAnswer,
typeof componentsSchemasJanuary,
typeof componentsSchemasFebruary,
typeof componentsSchemasMarch,
typeof componentsSchemasAnswer,
];
// schemas: array of JSON schemas to be registered with "fastify.addSchema"
export const schemas = [
componentsSchemasAnswer,
componentsSchemasJanuary,
componentsSchemasFebruary,
componentsSchemasMarch,
componentsSchemasAnswer,
]`);

expect(actualAsText).toBe(expectedAsText);
Expand All @@ -68,10 +68,10 @@ describe('fastifyIntegration plugin', () => {
);

expect(actual.schemas).toEqual([
answerSchema.with$id,
januarySchema.with$id,
februarySchema.with$id,
marchSchema.with$id,
answerSchema.with$id,
]);
});

Expand Down
Loading

0 comments on commit e63f47e

Please sign in to comment.