From 6ece5f88f01723ccf696720db0c46f2c5cde6f9f Mon Sep 17 00:00:00 2001 From: Andrea Carraro Date: Wed, 24 Apr 2024 10:51:59 +0200 Subject: [PATCH 1/2] refactor: organize schamas internally by id instead of --- .changeset/lazy-cows-repeat.md | 5 ++++ README.md | 4 +-- docs/developer-notes.md | 24 +++++++++++------- src/openapiToTsJsonSchema.ts | 25 +++++++++++-------- src/plugins/fastifyIntegrationPlugin.ts | 5 ++-- src/types.ts | 2 +- src/utils/addSchemaToMetaData.ts | 17 ++++++------- src/utils/idToPath.ts | 17 +++++++++++++ src/utils/index.ts | 9 ++++--- src/utils/makeTsJsonSchema/getId.ts | 15 +++++++++++ src/utils/makeTsJsonSchema/getRef.ts | 16 ------------ .../makeCircularRefReplacer.ts | 6 ++--- ...replaceInlinedRefsWithStringPlaceholder.ts | 18 ++++++------- .../replacePlaceholdersWithImportedSchemas.ts | 4 +-- .../replacePlaceholdersWithRefs.ts | 8 +++--- src/utils/parseId.ts | 12 +++++++++ src/utils/{pathToRef.ts => pathToId.ts} | 8 +++--- src/utils/refReplacementUtils.ts | 14 +++++------ src/utils/refToPath.ts | 19 -------------- test/circularReference.test.ts | 6 ++--- test/metaData.test.ts | 6 ++--- test/refHandling-keep.test.ts | 2 +- test/unit/addSchemaToMetaData.test.ts | 6 ++--- .../{refToPath.test.ts => idToPath.test.ts} | 6 ++--- test/unit/parseId.test.ts | 22 ++++++++++++++++ .../{pathToRef.test.ts => pathToId.test.ts} | 18 ++++++------- ...ceInlinedRefsWithStringPlaceholder.test.ts | 14 +++++------ 27 files changed, 175 insertions(+), 133 deletions(-) create mode 100644 .changeset/lazy-cows-repeat.md create mode 100644 src/utils/idToPath.ts create mode 100644 src/utils/makeTsJsonSchema/getId.ts delete mode 100644 src/utils/makeTsJsonSchema/getRef.ts create mode 100644 src/utils/parseId.ts rename src/utils/{pathToRef.ts => pathToId.ts} (75%) delete mode 100644 src/utils/refToPath.ts rename test/unit/{refToPath.test.ts => idToPath.test.ts} (65%) create mode 100644 test/unit/parseId.test.ts rename test/unit/{pathToRef.test.ts => pathToId.test.ts} (66%) diff --git a/.changeset/lazy-cows-repeat.md b/.changeset/lazy-cows-repeat.md new file mode 100644 index 0000000..6df1863 --- /dev/null +++ b/.changeset/lazy-cows-repeat.md @@ -0,0 +1,5 @@ +--- +"openapi-ts-json-schema": minor +--- + +`metaData.schemas` entry registered by id instead of `$ref` diff --git a/README.md b/README.md index 75ed666..0e29e88 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ if (validate(data)) { | **definitionPathsToGenerateFrom** _(required)_ | `string[]` | OpenApi definition object paths to generate the JSON schemas from. Only matching paths will be generated. Supports dot notation: `["components.schemas"]`. | - | | **refHandling** | | | | | **refHandling.strategy** | `"import" \| "inline" \| "keep"` | `"import"`: generate and import `$ref` schemas.
`"inline"`: inline `$ref` schemas.
`"keep"`: keep `$ref` values. | `"import"` | -| **refHandling.refMapper** | `(input: {ref: string}) => string` | Customize generated `$ref` values (only `keep` strategy) | - | +| **refHandling.refMapper** | `(input: {id: string}) => string` | Customize generated `$ref` values (only `keep` strategy) | - | | **schemaPatcher** | `(params: { schema: JSONSchema }) => void` | Dynamically patch generated JSON schemas. The provided function will be invoked against every single JSON schema node. | - | | **outputPath** | `string` | Path where the generated schemas will be saved. Defaults to `/schemas-autogenerated` in the same directory of `openApiSchema`. | - | | **plugins** | `ReturnType[]` | A set of optional plugins to generate extra custom output. See [plugins docs](./docs/plugins.md). | - | @@ -112,7 +112,7 @@ Beside generating the expected schema files under `outputPath`, `openapiToTsJson metaData: { // Meta data of the generated schemas schemas: Map< - // OpenAPI ref. Eg: "#/components/schemas/MySchema" + // Schema internal di. Eg: "/components/schemas/MySchema" string, { id: string; diff --git a/docs/developer-notes.md b/docs/developer-notes.md index 8dd85e3..c3a75dd 100644 --- a/docs/developer-notes.md +++ b/docs/developer-notes.md @@ -1,5 +1,13 @@ # Developer's notes +## Internal schema ids + +Each processed schemas is assigned with a unique internal id holding schema name and path information `//`. + +Eg: `/components/schemas/SchemaName`. + +Internal ids are used to refer to any specific schemas and retrieve schema path and name. + ## Remote $ref handling Remote/external `$ref`s (`Pet.yaml`, `definitions.json#/Pet`) get always immediately dereferenced by fetching the specs and inlining the relevant schemas. @@ -11,34 +19,34 @@ Remote/external `$ref`s (`Pet.yaml`, `definitions.json#/Pet`) get always immedia At the time of writing the implementation is build around `@apidevtools/json-schema-ref-parser`'s `dereference` method options and works as follows: 1. Schemas get deferenced with `@apidevtools/json-schema-ref-parser`'s `dereference` method which inlines relevant `$ref` schemas -2. Inlined schemas get marked with a symbol property holding the original `$ref` value (`#/foo/Bar`) +2. Inlined schemas get marked with a symbol property holding the internal schema id (`/components/schemas/Bar`) ```ts { bar: { - [Symbol('ref')]: '#/components/schemas/Bar', + [Symbol('id')]: '/components/schemas/Bar', // ...Inlined schema props } } ``` -3. Inlined and dereferenced schemas get traversed and all schemas marked with `Symbol('ref')` prop get replaced with a **string placeholder** holding the original `$ref` value. Note that string placeholders can be safely stringified. +1. Inlined and dereferenced schemas get traversed and all schemas marked with `Symbol('id')` prop get replaced with a **string placeholder** holding the original internal schema id. Note that string placeholders can be safely stringified. ```ts { - bar: '_OTJS-START_#/components/schemas/Bar_OTJS-END_'; + bar: '_OTJS-START_/components/schemas/Bar_OTJS-END_'; } ``` Note: alias definitions (eg. `Foo: "#components/schemas/Bar"`) will result in a plain **string placeholder**. ```ts -'_OTJS-START_#/components/schemas/Bar_OTJS-END_'; +'_OTJS-START_/components/schemas/Bar_OTJS-END_'; ``` -4. Inlined and dereferenced schemas get stringified and parsed to retrieve **string placeholders** and the contained original `$ref` value +1. Inlined and dereferenced schemas get stringified and parsed to retrieve **string placeholders** and their internal id value -5. For each **string placeholder** found, an import statement to the relevant `$ref` schema is prepended and the placeholder replaced with the imported schema name. +2. For each **string placeholder** found, an import statement to the relevant `$ref` schema is prepended and the placeholder replaced with the imported schema name. ```ts import Bar from '../foo/Bar'; @@ -48,8 +56,6 @@ export default { } as const ``` -This process could be definitely shorter if `@apidevtools/json-schema-ref-parser`'s `dereference` method allowed to access the parent object holding the `$ref` value to be replaced. In that case step 2 could be skipped and the ref object could be immediately replaced with the relevant **string placeholder**. - ## `refHandling`: keep `keep` option was implemented as last, and it currently follows the same flow as the `import` except for point 5, where schemas with **string placeholders** are replaced with the an actual `$ref` value. diff --git a/src/openapiToTsJsonSchema.ts b/src/openapiToTsJsonSchema.ts index b6bfba1..956c473 100644 --- a/src/openapiToTsJsonSchema.ts +++ b/src/openapiToTsJsonSchema.ts @@ -5,14 +5,15 @@ import get from 'lodash.get'; import { clearFolder, makeTsJsonSchemaFiles, - REF_SYMBOL, + SCHEMA_ID_SYMBOL, convertOpenApiToJsonSchema, convertOpenApiPathsParameters, addSchemaToMetaData, - pathToRef, + pathToId, formatTypeScript, saveFile, makeRelativeModulePath, + refToId, } from './utils'; import type { SchemaMetaDataMap, @@ -82,17 +83,19 @@ export async function openapiToTsJsonSchema( dereference: { // @ts-expect-error onDereference seems not to be properly typed onDereference: (ref, inlinedSchema) => { + const id = refToId(ref); + // Keep track of inlined refs - if (!inlinedRefs.has(ref)) { + if (!inlinedRefs.has(id)) { // Make a shallow copy of the ref schema to save it from the mutations below - inlinedRefs.set(ref, { ...inlinedSchema }); + inlinedRefs.set(id, { ...inlinedSchema }); /** * "import" refHandling support: - * mark inlined ref objects with a "REF_SYMBOL" to retrieve their + * mark inlined ref objects with a "SCHEMA_ID_SYMBOL" to retrieve their * original $ref value once inlined */ - inlinedSchema[REF_SYMBOL] = ref; + inlinedSchema[SCHEMA_ID_SYMBOL] = id; /** * "inline" refHandling support: @@ -122,9 +125,9 @@ export async function openapiToTsJsonSchema( * $ref schemas to be generated no matter of */ if (refHandling.strategy === 'import' || refHandling.strategy === 'keep') { - for (const [ref, schema] of inlinedRefs) { + for (const [id, schema] of inlinedRefs) { addSchemaToMetaData({ - ref, + id, schemaMetaDataMap, schema, outputPath, @@ -141,17 +144,17 @@ export async function openapiToTsJsonSchema( for (const schemaName in definitionSchemas) { // Create expected OpenAPI ref - const ref = pathToRef({ + const id = pathToId({ schemaRelativeDirName: definitionPath, schemaName, }); addSchemaToMetaData({ - ref, + id, schemaMetaDataMap, schema: definitionSchemas[schemaName], outputPath, - isRef: inlinedRefs.has(ref), + isRef: inlinedRefs.has(id), }); } } diff --git a/src/plugins/fastifyIntegrationPlugin.ts b/src/plugins/fastifyIntegrationPlugin.ts index 768b498..666c823 100644 --- a/src/plugins/fastifyIntegrationPlugin.ts +++ b/src/plugins/fastifyIntegrationPlugin.ts @@ -1,5 +1,4 @@ import type { Plugin } from '../types'; -import { refToId } from '../utils'; const OUTPUT_FILE_NAME = 'fastify-integration.ts'; const OPEN_API_COMPONENTS_SCHEMAS_PATH = '/components/schemas/'; @@ -19,12 +18,12 @@ const fastifyIntegrationPlugin: Plugin = ({ // Force "keep" refHandling options.refHandling = { strategy: 'keep', - refMapper: ({ ref }) => { + refMapper: ({ id }) => { /** * Replace original $ref values with internal schema id which * the schema is registered with via Fastify's `addSchema` */ - return refToId(ref); + return id; }, }; }, diff --git a/src/types.ts b/src/types.ts index 498ae2a..da57a9e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,7 +6,7 @@ export type SchemaPatcher = (params: { schema: JSONSchema }) => void; export type RefHandling = | { strategy: 'import' } | { strategy: 'inline' } - | { strategy: 'keep'; refMapper?: (input: { ref: string }) => string }; + | { strategy: 'keep'; refMapper?: (input: { id: string }) => string }; import type { makeRelativeModulePath, diff --git a/src/utils/addSchemaToMetaData.ts b/src/utils/addSchemaToMetaData.ts index a9208fd..c2206da 100644 --- a/src/utils/addSchemaToMetaData.ts +++ b/src/utils/addSchemaToMetaData.ts @@ -1,37 +1,36 @@ import path from 'node:path'; // @ts-expect-error no type defs for namify import namify from 'namify'; -import { parseRef, refToPath, filenamify, refToId } from '.'; +import { filenamify, idToPath } from '.'; import type { SchemaMetaDataMap, SchemaMetaData, JSONSchema } from '../types'; /* * Just an utility function to add entries to SchemaMetaDataMap Map keyed by ref */ export function addSchemaToMetaData({ - ref, + id, schemaMetaDataMap, schema, isRef, // Options outputPath, }: { - ref: string; + id: string; schemaMetaDataMap: SchemaMetaDataMap; schema: JSONSchema; isRef: boolean; outputPath: string; }): void { // Do not override existing meta info of inlined schemas - if (!schemaMetaDataMap.has(ref)) { - const refPath = parseRef(ref); - const { schemaRelativeDirName, schemaName } = refToPath(ref); + if (!schemaMetaDataMap.has(id)) { + const { schemaRelativeDirName, schemaName } = idToPath(id); const absoluteDirName = path.join(outputPath, schemaRelativeDirName); const schemaFileName = filenamify(schemaName); const absoluteImportPath = path.join(absoluteDirName, schemaFileName); const metaInfo: SchemaMetaData = { - id: refToId(ref), - uniqueName: namify(refPath), + id, + uniqueName: namify(id), isRef, originalSchema: schema, @@ -40,6 +39,6 @@ export function addSchemaToMetaData({ absolutePath: absoluteImportPath + '.ts', }; - schemaMetaDataMap.set(ref, metaInfo); + schemaMetaDataMap.set(id, metaInfo); } } diff --git a/src/utils/idToPath.ts b/src/utils/idToPath.ts new file mode 100644 index 0000000..155b8ca --- /dev/null +++ b/src/utils/idToPath.ts @@ -0,0 +1,17 @@ +import path from 'node:path'; +import { parseId } from './'; + +/** + * Parses internal schema ids (/components/schema/Foo) to the derive the expected schema output path + * this library saves generated JSON schemas to (...outputPath/components.schema/Foo) + */ +export function idToPath(id: string): { + schemaRelativeDirName: string; + schemaName: string; +} { + const idPath = parseId(id); + return { + schemaRelativeDirName: path.dirname(idPath), + schemaName: path.basename(idPath), + }; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index faa14be..149d12f 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -4,13 +4,14 @@ export { convertOpenApiPathsParameters } from './convertOpenApiPathsParameters'; export { convertOpenApiToJsonSchema } from './convertOpenApiToJsonSchema'; export { makeTsJsonSchemaFiles } from './makeTsJsonSchemaFiles'; export { parseRef } from './parseRef'; -export { refToPath } from './refToPath'; +export { parseId } from './parseId'; +export { idToPath } from './idToPath'; export { refToId } from './refToId'; -export { pathToRef } from './pathToRef'; +export { pathToId } from './pathToId'; export { - REF_SYMBOL, + SCHEMA_ID_SYMBOL, PLACEHOLDER_REGEX, - refToPlaceholder, + idToPlaceholder, } from './refReplacementUtils'; export { replaceInlinedRefsWithStringPlaceholder } from './makeTsJsonSchema/replaceInlinedRefsWithStringPlaceholder'; export { replacePlaceholdersWithImportedSchemas } from './makeTsJsonSchema/replacePlaceholdersWithImportedSchemas'; diff --git a/src/utils/makeTsJsonSchema/getId.ts b/src/utils/makeTsJsonSchema/getId.ts new file mode 100644 index 0000000..a64736a --- /dev/null +++ b/src/utils/makeTsJsonSchema/getId.ts @@ -0,0 +1,15 @@ +import { SCHEMA_ID_SYMBOL, isObject } from '..'; + +/** + * Retrieve SCHEMA_ID_SYMBOL prop value + */ +export function getId(node: unknown): string | undefined { + if ( + isObject(node) && + SCHEMA_ID_SYMBOL in node && + typeof node[SCHEMA_ID_SYMBOL] === 'string' + ) { + return node[SCHEMA_ID_SYMBOL]; + } + return undefined; +} diff --git a/src/utils/makeTsJsonSchema/getRef.ts b/src/utils/makeTsJsonSchema/getRef.ts deleted file mode 100644 index 67d45c5..0000000 --- a/src/utils/makeTsJsonSchema/getRef.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { REF_SYMBOL } from '..'; -import { isObject } from '../'; - -/** - * Retrieve REF_SYMBOL prop value - */ -export function getRef(node: unknown): string | undefined { - if ( - isObject(node) && - REF_SYMBOL in node && - typeof node[REF_SYMBOL] === 'string' - ) { - return node[REF_SYMBOL]; - } - return undefined; -} diff --git a/src/utils/makeTsJsonSchema/makeCircularRefReplacer.ts b/src/utils/makeTsJsonSchema/makeCircularRefReplacer.ts index a26c5d5..e0f16b7 100644 --- a/src/utils/makeTsJsonSchema/makeCircularRefReplacer.ts +++ b/src/utils/makeTsJsonSchema/makeCircularRefReplacer.ts @@ -1,4 +1,4 @@ -import { getRef } from './getRef'; +import { getId } from './getId'; /** * JSON.stringify replacer @@ -23,13 +23,13 @@ export function makeCircularRefReplacer(): ( // @NOTE Should we make recursion depth configurable? if (ancestors.includes(value)) { - const ref = getRef(value); + const id = getId(value); return { // Drop an inline comment about recursion interruption [Symbol.for('before')]: [ { type: 'LineComment', - value: ` Circular recursion interrupted (${ref})`, + value: ` Circular recursion interrupted. Schema id: "${id}"`, }, ], }; diff --git a/src/utils/makeTsJsonSchema/replaceInlinedRefsWithStringPlaceholder.ts b/src/utils/makeTsJsonSchema/replaceInlinedRefsWithStringPlaceholder.ts index 50435b4..6114ab9 100644 --- a/src/utils/makeTsJsonSchema/replaceInlinedRefsWithStringPlaceholder.ts +++ b/src/utils/makeTsJsonSchema/replaceInlinedRefsWithStringPlaceholder.ts @@ -1,32 +1,32 @@ import mapObject from 'map-obj'; -import { refToPlaceholder } from '..'; -import { getRef } from './getRef'; +import { idToPlaceholder } from '..'; +import { getId } from './getId'; import type { JSONSchema, JSONSchemaWithPlaceholders } from '../../types'; /** * Get any JSON schema node and: - * - Return ref placeholder is the entity is an inlined ref schema objects (with REF_SYMBOL prop) + * - Return ref placeholder is the entity is an inlined ref schema objects (with SCHEMA_ID_SYMBOL prop) * - Return provided node in all other cases */ function replaceInlinedSchemaWithPlaceholder( node: Node, ): Node | string { - const ref = getRef(node); - if (ref === undefined) { + const id = getId(node); + if (id === undefined) { return node; } - return refToPlaceholder(ref); + return idToPlaceholder(id); } /** * Iterate a JSON schema to replace inlined ref schema objects - * (marked with a REF_SYMBOL property holding the original $ref value) - * with a string placeholder with a reference to the original $ref value ("_OTJS-START_#/ref/value_OTJS-END_") + * (marked with a SCHEMA_ID_SYMBOL property holding the original $ref value) + * with a string placeholder with a reference to the original $ref value ("_OTJS-START_/id/value_OTJS-END_") */ export function replaceInlinedRefsWithStringPlaceholder( schema: JSONSchema, ): JSONSchemaWithPlaceholders { - if (getRef(schema)) { + if (getId(schema)) { return replaceInlinedSchemaWithPlaceholder(schema); } diff --git a/src/utils/makeTsJsonSchema/replacePlaceholdersWithImportedSchemas.ts b/src/utils/makeTsJsonSchema/replacePlaceholdersWithImportedSchemas.ts index 267680e..c48dcfc 100644 --- a/src/utils/makeTsJsonSchema/replacePlaceholdersWithImportedSchemas.ts +++ b/src/utils/makeTsJsonSchema/replacePlaceholdersWithImportedSchemas.ts @@ -16,8 +16,8 @@ export function replacePlaceholdersWithImportedSchemas({ const importStatements = new Set(); // Replace placeholder occurrences with the relevant imported schema name - let schema = schemaAsText.replaceAll(PLACEHOLDER_REGEX, (_match, ref) => { - const importedSchema = schemaMetaDataMap.get(ref); + let schema = schemaAsText.replaceAll(PLACEHOLDER_REGEX, (_match, id) => { + const importedSchema = schemaMetaDataMap.get(id); /* c8 ignore start */ if (!importedSchema) { diff --git a/src/utils/makeTsJsonSchema/replacePlaceholdersWithRefs.ts b/src/utils/makeTsJsonSchema/replacePlaceholdersWithRefs.ts index 7603149..263760a 100644 --- a/src/utils/makeTsJsonSchema/replacePlaceholdersWithRefs.ts +++ b/src/utils/makeTsJsonSchema/replacePlaceholdersWithRefs.ts @@ -5,14 +5,14 @@ import { PLACEHOLDER_REGEX } from '..'; */ export function replacePlaceholdersWithRefs({ schemaAsText, - refMapper = ({ ref }) => ref, + refMapper = ({ id }) => `#${id}`, }: { schemaAsText: string; - refMapper?: (input: { ref: string }) => string; + refMapper?: (input: { id: string }) => string; }): string { // Replace placeholder occurrences with a JSON schema $ref object - let schema = schemaAsText.replaceAll(PLACEHOLDER_REGEX, (_match, ref) => { - return `{ $ref: "${refMapper({ ref })}" }`; + let schema = schemaAsText.replaceAll(PLACEHOLDER_REGEX, (_match, id) => { + return `{ $ref: "${refMapper({ id })}" }`; }); return schema; diff --git a/src/utils/parseId.ts b/src/utils/parseId.ts new file mode 100644 index 0000000..5cc32c6 --- /dev/null +++ b/src/utils/parseId.ts @@ -0,0 +1,12 @@ +/** + * Parses internal schema ids: + * "/components/schema/Foo" --> "components/schema/Foo" + */ +export function parseId(id: string): string { + if (!id.startsWith('/')) { + throw new Error(`[openapi-ts-json-schema] Unsupported id value: "${id}"`); + } + + const idPath = id.replace('/', ''); + return idPath; +} diff --git a/src/utils/pathToRef.ts b/src/utils/pathToId.ts similarity index 75% rename from src/utils/pathToRef.ts rename to src/utils/pathToId.ts index bdd4248..d06e3c5 100644 --- a/src/utils/pathToRef.ts +++ b/src/utils/pathToId.ts @@ -1,11 +1,11 @@ import path from 'node:path'; -import { filenamify } from './'; +import { filenamify } from '.'; /** - * Generate a local OpenAPI ref from a relative path and a schema name + * Generate a local OpenAPI ref from a schema internal id */ const TRALING_SLASH_REGEX = /\/$/; -export function pathToRef({ +export function pathToId({ schemaRelativeDirName, schemaName, }: { @@ -13,7 +13,7 @@ export function pathToRef({ schemaName: string; }): string { return ( - '#/' + + '/' + path .normalize(schemaRelativeDirName) // Supporting definitionPathsToGenerateFrom dot notation diff --git a/src/utils/refReplacementUtils.ts b/src/utils/refReplacementUtils.ts index df4e6ca..c490385 100644 --- a/src/utils/refReplacementUtils.ts +++ b/src/utils/refReplacementUtils.ts @@ -1,16 +1,16 @@ -export const REF_SYMBOL = Symbol('ref'); +export const SCHEMA_ID_SYMBOL = Symbol('id'); -const REF_MARKER_START = '_OTJS-START_'; -const REF_MARKER_END = '_OTJS-END_'; +const SCHEMA_ID_MARKER_START = '_OTJS-START_'; +const SCHEMA_ID_MARKER_END = '_OTJS-END_'; export const PLACEHOLDER_REGEX = new RegExp( - `["']${REF_MARKER_START}(?.+)${REF_MARKER_END}["']`, + `["']${SCHEMA_ID_MARKER_START}(?.+)${SCHEMA_ID_MARKER_END}["']`, 'g', ); /** - * Generate a string placeholder containing the ref value to be retrieved later + * Generate a string placeholder containing the internal schema id value to be retrieved later */ -export function refToPlaceholder(ref: string): string { - return REF_MARKER_START + ref + REF_MARKER_END; +export function idToPlaceholder(id: string): string { + return SCHEMA_ID_MARKER_START + id + SCHEMA_ID_MARKER_END; } diff --git a/src/utils/refToPath.ts b/src/utils/refToPath.ts deleted file mode 100644 index bbe1b4e..0000000 --- a/src/utils/refToPath.ts +++ /dev/null @@ -1,19 +0,0 @@ -import path from 'node:path'; -import { parseRef } from '.'; - -/** - * Parses OpenAPI local refs (#/components/schema/Foo) to the derive the expected schema output path - * this library saves generated JSON schemas to (...outputPath/components.schema/Foo) - * - * @NOTE Remote and url refs should have been already resolved and inlined - */ -export function refToPath(ref: string): { - schemaRelativeDirName: string; - schemaName: string; -} { - const refPath = parseRef(ref); - return { - schemaRelativeDirName: path.dirname(refPath), - schemaName: path.basename(refPath), - }; -} diff --git a/test/circularReference.test.ts b/test/circularReference.test.ts index d5d3076..0cc7b3b 100644 --- a/test/circularReference.test.ts +++ b/test/circularReference.test.ts @@ -62,13 +62,13 @@ describe('Circular reference', () => { const expectedInlinedRef = ` nextMonth: { - // Circular recursion interrupted (#/components/schemas/February) + // Circular recursion interrupted. Schema id: "/components/schemas/February" }, nextMonthTwo: { - // Circular recursion interrupted (#/components/schemas/February) + // Circular recursion interrupted. Schema id: "/components/schemas/February" }, nextMonthThree: { - // Circular recursion interrupted (#/components/schemas/February) + // Circular recursion interrupted. Schema id: "/components/schemas/February" },`; expect(februarySchemaAsText).toEqual( diff --git a/test/metaData.test.ts b/test/metaData.test.ts index 656c89e..80506dc 100644 --- a/test/metaData.test.ts +++ b/test/metaData.test.ts @@ -15,10 +15,8 @@ describe('Returned "metaData"', async () => { silent: true, }); - const answerMetaData = metaData.schemas.get('#/components/schemas/Answer'); - const januaryMetaData = metaData.schemas.get( - '#/components/schemas/January', - ); + const answerMetaData = metaData.schemas.get('/components/schemas/Answer'); + const januaryMetaData = metaData.schemas.get('/components/schemas/January'); expect(answerMetaData).toBeDefined(); expect(januaryMetaData).toBeDefined(); diff --git a/test/refHandling-keep.test.ts b/test/refHandling-keep.test.ts index 94bd30c..241969a 100644 --- a/test/refHandling-keep.test.ts +++ b/test/refHandling-keep.test.ts @@ -155,7 +155,7 @@ describe('refHandling option === "keep"', () => { silent: true, refHandling: { strategy: 'keep', - refMapper: ({ ref }) => `foo_${ref}_bar`, + refMapper: ({ id }) => `foo_#${id}_bar`, }, }); diff --git a/test/unit/addSchemaToMetaData.test.ts b/test/unit/addSchemaToMetaData.test.ts index 800ee39..f37ecd7 100644 --- a/test/unit/addSchemaToMetaData.test.ts +++ b/test/unit/addSchemaToMetaData.test.ts @@ -5,7 +5,7 @@ import type { SchemaMetaData } from '../../src/types'; describe('addSchemaToMetaData', () => { it('generates expected metadata', () => { - const ref = '#/components/schemas/Foo'; + const id = '/components/schemas/Foo'; const schemaMetaDataMap = new Map(); const outputPath = path.normalize('/absolute/output/path'); const schema = { @@ -16,14 +16,14 @@ describe('addSchemaToMetaData', () => { }; addSchemaToMetaData({ - ref, + id, schemaMetaDataMap, schema, outputPath, isRef: true, }); - const actual = schemaMetaDataMap.get(ref); + const actual = schemaMetaDataMap.get(id); const expected: SchemaMetaData = { id: '/components/schemas/Foo', uniqueName: 'componentsSchemasFoo', diff --git a/test/unit/refToPath.test.ts b/test/unit/idToPath.test.ts similarity index 65% rename from test/unit/refToPath.test.ts rename to test/unit/idToPath.test.ts index cc1f6f7..f20810d 100644 --- a/test/unit/refToPath.test.ts +++ b/test/unit/idToPath.test.ts @@ -1,9 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { refToPath } from '../../src/utils'; +import { idToPath } from '../../src/utils'; -describe('refToPath', () => { +describe('idToPath', () => { it('generates expected ref paths', () => { - const actual = refToPath('#/components/schema/Foo'); + const actual = idToPath('/components/schema/Foo'); const expected = { schemaRelativeDirName: 'components/schema', schemaName: 'Foo', diff --git a/test/unit/parseId.test.ts b/test/unit/parseId.test.ts new file mode 100644 index 0000000..b88a7dc --- /dev/null +++ b/test/unit/parseId.test.ts @@ -0,0 +1,22 @@ +import { describe, it, expect } from 'vitest'; +import { parseId } from '../../src/utils'; + +describe('parseId', () => { + describe('Valid id', () => { + it('returns, ref path', () => { + const actual = parseId('/components/schemas/Foo'); + const expected = 'components/schemas/Foo'; + expect(actual).toBe(expected); + }); + }); + + describe('Invalid id', () => { + it('throws error', () => { + expect(() => parseId('#/components/schemas/Foo')).toThrow( + new Error( + `[openapi-ts-json-schema] Unsupported id value: "#/components/schemas/Foo"`, + ), + ); + }); + }); +}); diff --git a/test/unit/pathToRef.test.ts b/test/unit/pathToId.test.ts similarity index 66% rename from test/unit/pathToRef.test.ts rename to test/unit/pathToId.test.ts index 93859fe..afa3760 100644 --- a/test/unit/pathToRef.test.ts +++ b/test/unit/pathToId.test.ts @@ -1,38 +1,38 @@ import { describe, it, expect } from 'vitest'; -import { pathToRef } from '../../src/utils'; +import { pathToId } from '../../src/utils'; -describe('pathToRef', () => { +describe('pathToId', () => { it.each([ { schemaRelativeDirName: 'components/schemas', schemaName: 'Foo', - expected: '#/components/schemas/Foo', + expected: '/components/schemas/Foo', }, { schemaRelativeDirName: 'components/schemas/', schemaName: 'Foo', - expected: '#/components/schemas/Foo', + expected: '/components/schemas/Foo', }, { schemaRelativeDirName: 'components.schemas', schemaName: 'Foo', - expected: '#/components/schemas/Foo', + expected: '/components/schemas/Foo', }, // Windows path separators { schemaRelativeDirName: 'components\\schemas', schemaName: 'Foo', - expected: '#/components/schemas/Foo', + expected: '/components/schemas/Foo', }, { schemaRelativeDirName: 'components\\schemas\\', schemaName: 'Foo', - expected: '#/components/schemas/Foo', + expected: '/components/schemas/Foo', }, ])( - 'generates expected ref', + 'generates expected internal id', ({ schemaRelativeDirName, schemaName, expected }) => { - const actual = pathToRef({ + const actual = pathToId({ schemaRelativeDirName, schemaName, }); diff --git a/test/unit/replaceInlinedRefsWithStringPlaceholder.test.ts b/test/unit/replaceInlinedRefsWithStringPlaceholder.test.ts index e25090d..c7dc78a 100644 --- a/test/unit/replaceInlinedRefsWithStringPlaceholder.test.ts +++ b/test/unit/replaceInlinedRefsWithStringPlaceholder.test.ts @@ -1,21 +1,21 @@ import { describe, it, expect } from 'vitest'; import { replaceInlinedRefsWithStringPlaceholder, - REF_SYMBOL, + SCHEMA_ID_SYMBOL, } from '../../src/utils'; describe('replaceInlinedRefsWithStringPlaceholder', () => { - describe('nested object market with REF_SYMBOL', () => { + describe('nested object market with SCHEMA_ID_SYMBOL', () => { it('replaces objects with expected string placeholder', () => { const actual = replaceInlinedRefsWithStringPlaceholder({ schemas: { object: { foo: 'bar', - [REF_SYMBOL]: '#/ref/in/object', + [SCHEMA_ID_SYMBOL]: '#/ref/in/object', }, array: [ 'foo', - { hello: 'world', [REF_SYMBOL]: '#/ref/in/array' }, + { hello: 'world', [SCHEMA_ID_SYMBOL]: '#/ref/in/array' }, 'bar', ], }, @@ -32,12 +32,12 @@ describe('replaceInlinedRefsWithStringPlaceholder', () => { }); }); - describe('root object market with REF_SYMBOL (alias definitions)', () => { + describe('root object market with SCHEMA_ID_SYMBOL (alias definitions)', () => { it('replaces object with expected string placeholder', () => { - // @ts-expect-error REF_SYMBOL is not a valid JSON schema prop + // @ts-expect-error SCHEMA_ID_SYMBOL is not a valid JSON schema prop const actual = replaceInlinedRefsWithStringPlaceholder({ type: 'object', - [REF_SYMBOL]: '#/ref/in/root/object', + [SCHEMA_ID_SYMBOL]: '#/ref/in/root/object', }); const expected = '_OTJS-START_#/ref/in/root/object_OTJS-END_'; From 9350e0a7491fe0734acff014eabea1adc2e91c42 Mon Sep 17 00:00:00 2001 From: Andrea Carraro Date: Wed, 24 Apr 2024 11:15:57 +0200 Subject: [PATCH 2/2] refactor: utilities --- src/openapiToTsJsonSchema.ts | 4 ++-- src/utils/addSchemaToMetaData.ts | 4 ++-- src/utils/idToPath.ts | 17 ----------------- src/utils/index.ts | 4 +--- src/utils/{pathToId.ts => makeId.ts} | 2 +- src/utils/parseId.ts | 17 +++++++++++++---- src/utils/parseRef.ts | 12 ------------ src/utils/refToId.ts | 13 ++++++++++++- test/unit/idToPath.test.ts | 14 -------------- test/unit/{pathToId.test.ts => makeId.test.ts} | 6 +++--- test/unit/parseId.test.ts | 7 +++++-- test/unit/{parseRef.test.ts => refToId.test.ts} | 10 +++++----- ...laceInlinedRefsWithStringPlaceholder.test.ts | 12 ++++++------ 13 files changed, 50 insertions(+), 72 deletions(-) delete mode 100644 src/utils/idToPath.ts rename src/utils/{pathToId.ts => makeId.ts} (95%) delete mode 100644 src/utils/parseRef.ts delete mode 100644 test/unit/idToPath.test.ts rename test/unit/{pathToId.test.ts => makeId.test.ts} (90%) rename test/unit/{parseRef.test.ts => refToId.test.ts} (60%) diff --git a/src/openapiToTsJsonSchema.ts b/src/openapiToTsJsonSchema.ts index 956c473..704b5f9 100644 --- a/src/openapiToTsJsonSchema.ts +++ b/src/openapiToTsJsonSchema.ts @@ -9,7 +9,7 @@ import { convertOpenApiToJsonSchema, convertOpenApiPathsParameters, addSchemaToMetaData, - pathToId, + makeId, formatTypeScript, saveFile, makeRelativeModulePath, @@ -144,7 +144,7 @@ export async function openapiToTsJsonSchema( for (const schemaName in definitionSchemas) { // Create expected OpenAPI ref - const id = pathToId({ + const id = makeId({ schemaRelativeDirName: definitionPath, schemaName, }); diff --git a/src/utils/addSchemaToMetaData.ts b/src/utils/addSchemaToMetaData.ts index c2206da..f1b0b62 100644 --- a/src/utils/addSchemaToMetaData.ts +++ b/src/utils/addSchemaToMetaData.ts @@ -1,7 +1,7 @@ import path from 'node:path'; // @ts-expect-error no type defs for namify import namify from 'namify'; -import { filenamify, idToPath } from '.'; +import { filenamify, parseId } from '.'; import type { SchemaMetaDataMap, SchemaMetaData, JSONSchema } from '../types'; /* @@ -23,7 +23,7 @@ export function addSchemaToMetaData({ }): void { // Do not override existing meta info of inlined schemas if (!schemaMetaDataMap.has(id)) { - const { schemaRelativeDirName, schemaName } = idToPath(id); + const { schemaRelativeDirName, schemaName } = parseId(id); const absoluteDirName = path.join(outputPath, schemaRelativeDirName); const schemaFileName = filenamify(schemaName); const absoluteImportPath = path.join(absoluteDirName, schemaFileName); diff --git a/src/utils/idToPath.ts b/src/utils/idToPath.ts deleted file mode 100644 index 155b8ca..0000000 --- a/src/utils/idToPath.ts +++ /dev/null @@ -1,17 +0,0 @@ -import path from 'node:path'; -import { parseId } from './'; - -/** - * Parses internal schema ids (/components/schema/Foo) to the derive the expected schema output path - * this library saves generated JSON schemas to (...outputPath/components.schema/Foo) - */ -export function idToPath(id: string): { - schemaRelativeDirName: string; - schemaName: string; -} { - const idPath = parseId(id); - return { - schemaRelativeDirName: path.dirname(idPath), - schemaName: path.basename(idPath), - }; -} diff --git a/src/utils/index.ts b/src/utils/index.ts index 149d12f..dd65883 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -3,11 +3,9 @@ export { makeTsJsonSchema } from './makeTsJsonSchema'; export { convertOpenApiPathsParameters } from './convertOpenApiPathsParameters'; export { convertOpenApiToJsonSchema } from './convertOpenApiToJsonSchema'; export { makeTsJsonSchemaFiles } from './makeTsJsonSchemaFiles'; -export { parseRef } from './parseRef'; export { parseId } from './parseId'; -export { idToPath } from './idToPath'; export { refToId } from './refToId'; -export { pathToId } from './pathToId'; +export { makeId } from './makeId'; export { SCHEMA_ID_SYMBOL, PLACEHOLDER_REGEX, diff --git a/src/utils/pathToId.ts b/src/utils/makeId.ts similarity index 95% rename from src/utils/pathToId.ts rename to src/utils/makeId.ts index d06e3c5..3e122d4 100644 --- a/src/utils/pathToId.ts +++ b/src/utils/makeId.ts @@ -5,7 +5,7 @@ import { filenamify } from '.'; * Generate a local OpenAPI ref from a schema internal id */ const TRALING_SLASH_REGEX = /\/$/; -export function pathToId({ +export function makeId({ schemaRelativeDirName, schemaName, }: { diff --git a/src/utils/parseId.ts b/src/utils/parseId.ts index 5cc32c6..dc6bd30 100644 --- a/src/utils/parseId.ts +++ b/src/utils/parseId.ts @@ -1,12 +1,21 @@ +import path from 'node:path'; + /** - * Parses internal schema ids: - * "/components/schema/Foo" --> "components/schema/Foo" + * Parses internal schema ids (/components/schema/Foo) to the derive the expected schema output path + * this library saves generated JSON schemas to (...outputPath/components.schema/Foo) */ -export function parseId(id: string): string { +export function parseId(id: string): { + schemaRelativeDirName: string; + schemaName: string; +} { if (!id.startsWith('/')) { throw new Error(`[openapi-ts-json-schema] Unsupported id value: "${id}"`); } const idPath = id.replace('/', ''); - return idPath; + + return { + schemaRelativeDirName: path.dirname(idPath), + schemaName: path.basename(idPath), + }; } diff --git a/src/utils/parseRef.ts b/src/utils/parseRef.ts deleted file mode 100644 index 2fdf9cb..0000000 --- a/src/utils/parseRef.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Parses OpenApi ref: - * "#/components/schema/Foo" --> "components/schema/Foo" - */ -export function parseRef(ref: string): string { - if (!ref.startsWith('#/')) { - throw new Error(`[openapi-ts-json-schema] Unsupported ref value: "${ref}"`); - } - - const refPath = ref.replace('#/', ''); - return refPath; -} diff --git a/src/utils/refToId.ts b/src/utils/refToId.ts index dedc401..558a149 100644 --- a/src/utils/refToId.ts +++ b/src/utils/refToId.ts @@ -1,4 +1,15 @@ -import { parseRef } from '.'; +/** + * Parses OpenApi ref: + * "#/components/schema/Foo" --> "components/schema/Foo" + */ +function parseRef(ref: string): string { + if (!ref.startsWith('#/')) { + throw new Error(`[openapi-ts-json-schema] Unsupported ref value: "${ref}"`); + } + + const refPath = ref.replace('#/', ''); + return refPath; +} /** * Generate an internal schema ID from a given schema ref: diff --git a/test/unit/idToPath.test.ts b/test/unit/idToPath.test.ts deleted file mode 100644 index f20810d..0000000 --- a/test/unit/idToPath.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { idToPath } from '../../src/utils'; - -describe('idToPath', () => { - it('generates expected ref paths', () => { - const actual = idToPath('/components/schema/Foo'); - const expected = { - schemaRelativeDirName: 'components/schema', - schemaName: 'Foo', - }; - - expect(actual).toEqual(expected); - }); -}); diff --git a/test/unit/pathToId.test.ts b/test/unit/makeId.test.ts similarity index 90% rename from test/unit/pathToId.test.ts rename to test/unit/makeId.test.ts index afa3760..9d5842a 100644 --- a/test/unit/pathToId.test.ts +++ b/test/unit/makeId.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from 'vitest'; -import { pathToId } from '../../src/utils'; +import { makeId } from '../../src/utils'; -describe('pathToId', () => { +describe('makeId', () => { it.each([ { schemaRelativeDirName: 'components/schemas', @@ -32,7 +32,7 @@ describe('pathToId', () => { ])( 'generates expected internal id', ({ schemaRelativeDirName, schemaName, expected }) => { - const actual = pathToId({ + const actual = makeId({ schemaRelativeDirName, schemaName, }); diff --git a/test/unit/parseId.test.ts b/test/unit/parseId.test.ts index b88a7dc..df5f7d7 100644 --- a/test/unit/parseId.test.ts +++ b/test/unit/parseId.test.ts @@ -5,8 +5,11 @@ describe('parseId', () => { describe('Valid id', () => { it('returns, ref path', () => { const actual = parseId('/components/schemas/Foo'); - const expected = 'components/schemas/Foo'; - expect(actual).toBe(expected); + const expected = { + schemaRelativeDirName: 'components/schemas', + schemaName: 'Foo', + }; + expect(actual).toEqual(expected); }); }); diff --git a/test/unit/parseRef.test.ts b/test/unit/refToId.test.ts similarity index 60% rename from test/unit/parseRef.test.ts rename to test/unit/refToId.test.ts index c041892..6675c2f 100644 --- a/test/unit/parseRef.test.ts +++ b/test/unit/refToId.test.ts @@ -1,18 +1,18 @@ import { describe, it, expect } from 'vitest'; -import { parseRef } from '../../src/utils'; +import { refToId } from '../../src/utils'; -describe('parseRef', () => { +describe('refToId', () => { describe('Valid ref', () => { it('returns, ref path', () => { - const actual = parseRef('#/components/schemas/Foo'); - const expected = 'components/schemas/Foo'; + const actual = refToId('#/components/schemas/Foo'); + const expected = '/components/schemas/Foo'; expect(actual).toBe(expected); }); }); describe('Invalid ref', () => { it('throws error', () => { - expect(() => parseRef('/components/schemas/Foo')).toThrow( + expect(() => refToId('/components/schemas/Foo')).toThrow( new Error( `[openapi-ts-json-schema] Unsupported ref value: "/components/schemas/Foo"`, ), diff --git a/test/unit/replaceInlinedRefsWithStringPlaceholder.test.ts b/test/unit/replaceInlinedRefsWithStringPlaceholder.test.ts index c7dc78a..4aa8977 100644 --- a/test/unit/replaceInlinedRefsWithStringPlaceholder.test.ts +++ b/test/unit/replaceInlinedRefsWithStringPlaceholder.test.ts @@ -11,11 +11,11 @@ describe('replaceInlinedRefsWithStringPlaceholder', () => { schemas: { object: { foo: 'bar', - [SCHEMA_ID_SYMBOL]: '#/ref/in/object', + [SCHEMA_ID_SYMBOL]: '/ref/in/object', }, array: [ 'foo', - { hello: 'world', [SCHEMA_ID_SYMBOL]: '#/ref/in/array' }, + { hello: 'world', [SCHEMA_ID_SYMBOL]: '/ref/in/array' }, 'bar', ], }, @@ -23,8 +23,8 @@ describe('replaceInlinedRefsWithStringPlaceholder', () => { const expected = { schemas: { - object: '_OTJS-START_#/ref/in/object_OTJS-END_', - array: ['foo', '_OTJS-START_#/ref/in/array_OTJS-END_', 'bar'], + object: '_OTJS-START_/ref/in/object_OTJS-END_', + array: ['foo', '_OTJS-START_/ref/in/array_OTJS-END_', 'bar'], }, }; @@ -37,9 +37,9 @@ describe('replaceInlinedRefsWithStringPlaceholder', () => { // @ts-expect-error SCHEMA_ID_SYMBOL is not a valid JSON schema prop const actual = replaceInlinedRefsWithStringPlaceholder({ type: 'object', - [SCHEMA_ID_SYMBOL]: '#/ref/in/root/object', + [SCHEMA_ID_SYMBOL]: '/ref/in/root/object', }); - const expected = '_OTJS-START_#/ref/in/root/object_OTJS-END_'; + const expected = '_OTJS-START_/ref/in/root/object_OTJS-END_'; expect(actual).toEqual(expected); });