Skip to content

Commit 0faeec1

Browse files
Merge pull request #546 from bitgopatmcl/refactor-ref-recursion
fix: refactor ref recursion
2 parents 33dd991 + 36475fa commit 0faeec1

File tree

5 files changed

+225
-43
lines changed

5 files changed

+225
-43
lines changed

packages/openapi-generator/src/cli.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ import * as fs from 'fs';
1515
import * as p from 'path';
1616

1717
import { parseApiSpec } from './apiSpec';
18-
import { Components, parseRefs } from './ref';
18+
import { getRefs } from './ref';
1919
import { convertRoutesToOpenAPI } from './openapi';
2020
import type { Route } from './route';
2121
import type { Schema } from './ir';
2222
import { Project } from './project';
2323
import { KNOWN_IMPORTS } from './knownImports';
24+
import { findSymbolInitializer } from './resolveInit';
25+
import { parseCodecInitializer } from './codec';
2426

2527
const app = command({
2628
name: 'api-ts',
@@ -130,7 +132,7 @@ const app = command({
130132
process.exit(1);
131133
}
132134

133-
const components: Components = {};
135+
const components: Record<string, Schema> = {};
134136
const queue: Schema[] = apiSpec.flatMap((route) => {
135137
return [
136138
...route.parameters.map((p) => p.schema),
@@ -140,13 +142,33 @@ const app = command({
140142
});
141143
let schema: Schema | undefined;
142144
while (((schema = queue.pop()), schema !== undefined)) {
143-
const newComponents = parseRefs(project.right, schema);
144-
for (const [name, schema] of Object.entries(newComponents)) {
145-
if (components[name] !== undefined) {
145+
const refs = getRefs(schema);
146+
for (const ref of refs) {
147+
if (components[ref.name] !== undefined) {
146148
continue;
147149
}
148-
components[name] = schema;
149-
queue.push(schema);
150+
const sourceFile = project.right.get(ref.location);
151+
if (sourceFile === undefined) {
152+
console.error(`Could not find source file '${ref.location}'`);
153+
process.exit(1);
154+
}
155+
const initE = findSymbolInitializer(project.right, sourceFile, ref.name);
156+
if (E.isLeft(initE)) {
157+
console.error(
158+
`Could not find symbol '${ref.name}' in '${ref.location}': ${initE.left}`,
159+
);
160+
process.exit(1);
161+
}
162+
const [newSourceFile, init] = initE.right;
163+
const codecE = parseCodecInitializer(project.right, newSourceFile, init);
164+
if (E.isLeft(codecE)) {
165+
console.error(
166+
`Could not parse codec '${ref.name}' in '${ref.location}': ${codecE.left}`,
167+
);
168+
process.exit(1);
169+
}
170+
components[ref.name] = codecE.right;
171+
queue.push(codecE.right);
150172
}
151173
}
152174

packages/openapi-generator/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export { parseCodecInitializer, parsePlainInitializer } from './codec';
33
export { parseCommentBlock, type JSDoc } from './jsdoc';
44
export { convertRoutesToOpenAPI } from './openapi';
55
export { Project } from './project';
6+
export { getRefs } from './ref';
67
export { parseRoute, type Route } from './route';
78
export { parseSource } from './sourceFile';
89
export { parseTopLevelSymbols } from './symbol';

packages/openapi-generator/src/openapi.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { OpenAPIV3 } from 'openapi-types';
22

33
import { STATUS_CODES } from 'http';
44
import { parseCommentBlock } from './jsdoc';
5-
import type { Components } from './ref';
65
import type { Route } from './route';
76
import type { Schema } from './ir';
87

@@ -150,7 +149,7 @@ function routeToOpenAPI(route: Route): [string, string, OpenAPIV3.OperationObjec
150149
export function convertRoutesToOpenAPI(
151150
info: OpenAPIV3.InfoObject,
152151
routes: Route[],
153-
schemas: Components,
152+
schemas: Record<string, Schema>,
154153
): OpenAPIV3.Document {
155154
const paths = routes.reduce(
156155
(acc, route) => {
Lines changed: 17 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,25 @@
1-
import * as E from 'fp-ts/Either';
1+
import type { Schema, Reference } from './ir';
22

3-
import { parseCodecInitializer } from './codec';
4-
import type { Project } from './project';
5-
import type { Schema } from './ir';
6-
import { findSymbolInitializer } from './resolveInit';
7-
8-
export type Components = Record<string, Schema>;
9-
10-
export function parseRefs(project: Project, schema: Schema): Record<string, Schema> {
3+
export function getRefs(schema: Schema): Reference[] {
114
if (schema.type === 'ref') {
12-
const sourceFile = project.get(schema.location);
13-
if (sourceFile === undefined) {
14-
return {};
15-
}
16-
const initE = findSymbolInitializer(project, sourceFile, schema.name);
17-
if (E.isLeft(initE)) {
18-
return {};
19-
}
20-
const [newSourceFile, init] = initE.right;
21-
const codecE = parseCodecInitializer(project, newSourceFile, init);
22-
if (E.isLeft(codecE)) {
23-
return {};
24-
}
25-
const codec = codecE.right;
26-
return { [schema.name]: codec };
5+
return [schema];
276
} else if (schema.type === 'array') {
28-
return parseRefs(project, schema.items);
29-
} else if (schema.type === 'intersection' || schema.type === 'union') {
30-
return schema.schemas.reduce((acc, member) => {
31-
return { ...acc, ...parseRefs(project, member) };
32-
}, {});
7+
return getRefs(schema.items);
8+
} else if (
9+
schema.type === 'intersection' ||
10+
schema.type === 'union' ||
11+
schema.type === 'tuple'
12+
) {
13+
return schema.schemas.reduce<Reference[]>((acc, member) => {
14+
return [...acc, ...getRefs(member)];
15+
}, []);
3316
} else if (schema.type === 'object') {
34-
return Object.values(schema.properties).reduce((acc, member) => {
35-
return { ...acc, ...parseRefs(project, member) };
36-
}, {});
17+
return Object.values(schema.properties).reduce<Reference[]>((acc, member) => {
18+
return [...acc, ...getRefs(member)];
19+
}, []);
3720
} else if (schema.type === 'record') {
38-
return parseRefs(project, schema.codomain);
21+
return getRefs(schema.codomain);
3922
} else {
40-
return {};
23+
return [];
4124
}
4225
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import assert from 'node:assert';
2+
import test from 'node:test';
3+
4+
import { getRefs, type Schema } from '../src';
5+
6+
test('simple ref is returned', () => {
7+
const schema: Schema = {
8+
type: 'ref',
9+
name: 'Foo',
10+
location: '/foo.ts',
11+
};
12+
13+
assert.deepStrictEqual(getRefs(schema), [schema]);
14+
});
15+
16+
test('array ref is returned', () => {
17+
const schema: Schema = {
18+
type: 'array',
19+
items: {
20+
type: 'ref',
21+
name: 'Foo',
22+
location: '/foo.ts',
23+
},
24+
};
25+
26+
assert.deepStrictEqual(getRefs(schema), [
27+
{
28+
type: 'ref',
29+
name: 'Foo',
30+
location: '/foo.ts',
31+
},
32+
]);
33+
});
34+
35+
test('intersection ref is returned', () => {
36+
const schema: Schema = {
37+
type: 'intersection',
38+
schemas: [
39+
{
40+
type: 'ref',
41+
name: 'Foo',
42+
location: '/foo.ts',
43+
},
44+
{
45+
type: 'ref',
46+
name: 'Bar',
47+
location: '/bar.ts',
48+
},
49+
],
50+
};
51+
52+
assert.deepStrictEqual(getRefs(schema), [
53+
{
54+
type: 'ref',
55+
name: 'Foo',
56+
location: '/foo.ts',
57+
},
58+
{
59+
type: 'ref',
60+
name: 'Bar',
61+
location: '/bar.ts',
62+
},
63+
]);
64+
});
65+
66+
test('union ref is returned', () => {
67+
const schema: Schema = {
68+
type: 'union',
69+
schemas: [
70+
{
71+
type: 'ref',
72+
name: 'Foo',
73+
location: '/foo.ts',
74+
},
75+
{
76+
type: 'ref',
77+
name: 'Bar',
78+
location: '/bar.ts',
79+
},
80+
],
81+
};
82+
83+
assert.deepStrictEqual(getRefs(schema), [
84+
{
85+
type: 'ref',
86+
name: 'Foo',
87+
location: '/foo.ts',
88+
},
89+
{
90+
type: 'ref',
91+
name: 'Bar',
92+
location: '/bar.ts',
93+
},
94+
]);
95+
});
96+
97+
test('tuple ref is returned', () => {
98+
const schema: Schema = {
99+
type: 'tuple',
100+
schemas: [
101+
{
102+
type: 'ref',
103+
name: 'Foo',
104+
location: '/foo.ts',
105+
},
106+
{
107+
type: 'ref',
108+
name: 'Bar',
109+
location: '/bar.ts',
110+
},
111+
],
112+
};
113+
114+
assert.deepStrictEqual(getRefs(schema), [
115+
{
116+
type: 'ref',
117+
name: 'Foo',
118+
location: '/foo.ts',
119+
},
120+
{
121+
type: 'ref',
122+
name: 'Bar',
123+
location: '/bar.ts',
124+
},
125+
]);
126+
});
127+
128+
test('object ref is returned', () => {
129+
const schema: Schema = {
130+
type: 'object',
131+
properties: {
132+
foo: {
133+
type: 'ref',
134+
name: 'Foo',
135+
location: '/foo.ts',
136+
},
137+
bar: {
138+
type: 'ref',
139+
name: 'Bar',
140+
location: '/bar.ts',
141+
},
142+
},
143+
required: ['foo', 'bar'],
144+
};
145+
146+
assert.deepStrictEqual(getRefs(schema), [
147+
{
148+
type: 'ref',
149+
name: 'Foo',
150+
location: '/foo.ts',
151+
},
152+
{
153+
type: 'ref',
154+
name: 'Bar',
155+
location: '/bar.ts',
156+
},
157+
]);
158+
});
159+
160+
test('record ref is returned', () => {
161+
const schema: Schema = {
162+
type: 'record',
163+
codomain: {
164+
type: 'ref',
165+
name: 'Foo',
166+
location: '/foo.ts',
167+
},
168+
};
169+
170+
assert.deepStrictEqual(getRefs(schema), [
171+
{
172+
type: 'ref',
173+
name: 'Foo',
174+
location: '/foo.ts',
175+
},
176+
]);
177+
});

0 commit comments

Comments
 (0)