From 36475fa06495a09203ddd5bda27772da397778ad Mon Sep 17 00:00:00 2001 From: Patrick McLaughlin Date: Fri, 8 Sep 2023 11:53:09 -0400 Subject: [PATCH] fix: refactor ref recursion --- packages/openapi-generator/src/cli.ts | 36 +++- packages/openapi-generator/src/index.ts | 1 + packages/openapi-generator/src/openapi.ts | 3 +- packages/openapi-generator/src/ref.ts | 51 ++---- packages/openapi-generator/test/ref.test.ts | 177 ++++++++++++++++++++ 5 files changed, 225 insertions(+), 43 deletions(-) create mode 100644 packages/openapi-generator/test/ref.test.ts diff --git a/packages/openapi-generator/src/cli.ts b/packages/openapi-generator/src/cli.ts index 0437538c..36085cdf 100644 --- a/packages/openapi-generator/src/cli.ts +++ b/packages/openapi-generator/src/cli.ts @@ -15,12 +15,14 @@ import * as fs from 'fs'; import * as p from 'path'; import { parseApiSpec } from './apiSpec'; -import { Components, parseRefs } from './ref'; +import { getRefs } from './ref'; import { convertRoutesToOpenAPI } from './openapi'; import type { Route } from './route'; import type { Schema } from './ir'; import { Project } from './project'; import { KNOWN_IMPORTS } from './knownImports'; +import { findSymbolInitializer } from './resolveInit'; +import { parseCodecInitializer } from './codec'; const app = command({ name: 'api-ts', @@ -130,7 +132,7 @@ const app = command({ process.exit(1); } - const components: Components = {}; + const components: Record = {}; const queue: Schema[] = apiSpec.flatMap((route) => { return [ ...route.parameters.map((p) => p.schema), @@ -140,13 +142,33 @@ const app = command({ }); let schema: Schema | undefined; while (((schema = queue.pop()), schema !== undefined)) { - const newComponents = parseRefs(project.right, schema); - for (const [name, schema] of Object.entries(newComponents)) { - if (components[name] !== undefined) { + const refs = getRefs(schema); + for (const ref of refs) { + if (components[ref.name] !== undefined) { continue; } - components[name] = schema; - queue.push(schema); + const sourceFile = project.right.get(ref.location); + if (sourceFile === undefined) { + console.error(`Could not find source file '${ref.location}'`); + process.exit(1); + } + const initE = findSymbolInitializer(project.right, sourceFile, ref.name); + if (E.isLeft(initE)) { + console.error( + `Could not find symbol '${ref.name}' in '${ref.location}': ${initE.left}`, + ); + process.exit(1); + } + const [newSourceFile, init] = initE.right; + const codecE = parseCodecInitializer(project.right, newSourceFile, init); + if (E.isLeft(codecE)) { + console.error( + `Could not parse codec '${ref.name}' in '${ref.location}': ${codecE.left}`, + ); + process.exit(1); + } + components[ref.name] = codecE.right; + queue.push(codecE.right); } } diff --git a/packages/openapi-generator/src/index.ts b/packages/openapi-generator/src/index.ts index 3ee82453..6ccb8dee 100644 --- a/packages/openapi-generator/src/index.ts +++ b/packages/openapi-generator/src/index.ts @@ -3,6 +3,7 @@ export { parseCodecInitializer, parsePlainInitializer } from './codec'; export { parseCommentBlock, type JSDoc } from './jsdoc'; export { convertRoutesToOpenAPI } from './openapi'; export { Project } from './project'; +export { getRefs } from './ref'; export { parseRoute, type Route } from './route'; export { parseSource } from './sourceFile'; export { parseTopLevelSymbols } from './symbol'; diff --git a/packages/openapi-generator/src/openapi.ts b/packages/openapi-generator/src/openapi.ts index 1f840134..53de3cc1 100644 --- a/packages/openapi-generator/src/openapi.ts +++ b/packages/openapi-generator/src/openapi.ts @@ -2,7 +2,6 @@ import { OpenAPIV3 } from 'openapi-types'; import { STATUS_CODES } from 'http'; import { parseCommentBlock } from './jsdoc'; -import type { Components } from './ref'; import type { Route } from './route'; import type { Schema } from './ir'; @@ -150,7 +149,7 @@ function routeToOpenAPI(route: Route): [string, string, OpenAPIV3.OperationObjec export function convertRoutesToOpenAPI( info: OpenAPIV3.InfoObject, routes: Route[], - schemas: Components, + schemas: Record, ): OpenAPIV3.Document { const paths = routes.reduce( (acc, route) => { diff --git a/packages/openapi-generator/src/ref.ts b/packages/openapi-generator/src/ref.ts index 4f5dd8b0..aadfa57a 100644 --- a/packages/openapi-generator/src/ref.ts +++ b/packages/openapi-generator/src/ref.ts @@ -1,42 +1,25 @@ -import * as E from 'fp-ts/Either'; +import type { Schema, Reference } from './ir'; -import { parseCodecInitializer } from './codec'; -import type { Project } from './project'; -import type { Schema } from './ir'; -import { findSymbolInitializer } from './resolveInit'; - -export type Components = Record; - -export function parseRefs(project: Project, schema: Schema): Record { +export function getRefs(schema: Schema): Reference[] { if (schema.type === 'ref') { - const sourceFile = project.get(schema.location); - if (sourceFile === undefined) { - return {}; - } - const initE = findSymbolInitializer(project, sourceFile, schema.name); - if (E.isLeft(initE)) { - return {}; - } - const [newSourceFile, init] = initE.right; - const codecE = parseCodecInitializer(project, newSourceFile, init); - if (E.isLeft(codecE)) { - return {}; - } - const codec = codecE.right; - return { [schema.name]: codec }; + return [schema]; } else if (schema.type === 'array') { - return parseRefs(project, schema.items); - } else if (schema.type === 'intersection' || schema.type === 'union') { - return schema.schemas.reduce((acc, member) => { - return { ...acc, ...parseRefs(project, member) }; - }, {}); + return getRefs(schema.items); + } else if ( + schema.type === 'intersection' || + schema.type === 'union' || + schema.type === 'tuple' + ) { + return schema.schemas.reduce((acc, member) => { + return [...acc, ...getRefs(member)]; + }, []); } else if (schema.type === 'object') { - return Object.values(schema.properties).reduce((acc, member) => { - return { ...acc, ...parseRefs(project, member) }; - }, {}); + return Object.values(schema.properties).reduce((acc, member) => { + return [...acc, ...getRefs(member)]; + }, []); } else if (schema.type === 'record') { - return parseRefs(project, schema.codomain); + return getRefs(schema.codomain); } else { - return {}; + return []; } } diff --git a/packages/openapi-generator/test/ref.test.ts b/packages/openapi-generator/test/ref.test.ts new file mode 100644 index 00000000..6890a5b1 --- /dev/null +++ b/packages/openapi-generator/test/ref.test.ts @@ -0,0 +1,177 @@ +import assert from 'node:assert'; +import test from 'node:test'; + +import { getRefs, type Schema } from '../src'; + +test('simple ref is returned', () => { + const schema: Schema = { + type: 'ref', + name: 'Foo', + location: '/foo.ts', + }; + + assert.deepStrictEqual(getRefs(schema), [schema]); +}); + +test('array ref is returned', () => { + const schema: Schema = { + type: 'array', + items: { + type: 'ref', + name: 'Foo', + location: '/foo.ts', + }, + }; + + assert.deepStrictEqual(getRefs(schema), [ + { + type: 'ref', + name: 'Foo', + location: '/foo.ts', + }, + ]); +}); + +test('intersection ref is returned', () => { + const schema: Schema = { + type: 'intersection', + schemas: [ + { + type: 'ref', + name: 'Foo', + location: '/foo.ts', + }, + { + type: 'ref', + name: 'Bar', + location: '/bar.ts', + }, + ], + }; + + assert.deepStrictEqual(getRefs(schema), [ + { + type: 'ref', + name: 'Foo', + location: '/foo.ts', + }, + { + type: 'ref', + name: 'Bar', + location: '/bar.ts', + }, + ]); +}); + +test('union ref is returned', () => { + const schema: Schema = { + type: 'union', + schemas: [ + { + type: 'ref', + name: 'Foo', + location: '/foo.ts', + }, + { + type: 'ref', + name: 'Bar', + location: '/bar.ts', + }, + ], + }; + + assert.deepStrictEqual(getRefs(schema), [ + { + type: 'ref', + name: 'Foo', + location: '/foo.ts', + }, + { + type: 'ref', + name: 'Bar', + location: '/bar.ts', + }, + ]); +}); + +test('tuple ref is returned', () => { + const schema: Schema = { + type: 'tuple', + schemas: [ + { + type: 'ref', + name: 'Foo', + location: '/foo.ts', + }, + { + type: 'ref', + name: 'Bar', + location: '/bar.ts', + }, + ], + }; + + assert.deepStrictEqual(getRefs(schema), [ + { + type: 'ref', + name: 'Foo', + location: '/foo.ts', + }, + { + type: 'ref', + name: 'Bar', + location: '/bar.ts', + }, + ]); +}); + +test('object ref is returned', () => { + const schema: Schema = { + type: 'object', + properties: { + foo: { + type: 'ref', + name: 'Foo', + location: '/foo.ts', + }, + bar: { + type: 'ref', + name: 'Bar', + location: '/bar.ts', + }, + }, + required: ['foo', 'bar'], + }; + + assert.deepStrictEqual(getRefs(schema), [ + { + type: 'ref', + name: 'Foo', + location: '/foo.ts', + }, + { + type: 'ref', + name: 'Bar', + location: '/bar.ts', + }, + ]); +}); + +test('record ref is returned', () => { + const schema: Schema = { + type: 'record', + codomain: { + type: 'ref', + name: 'Foo', + location: '/foo.ts', + }, + }; + + assert.deepStrictEqual(getRefs(schema), [ + { + type: 'ref', + name: 'Foo', + location: '/foo.ts', + }, + ]); +});