Skip to content

Commit 2ac5086

Browse files
Merge pull request #560 from bitgopatmcl/codec-refs-inside-object-literals
feat: support reading codecs directly ref-d from objects
2 parents f40856e + 8471732 commit 2ac5086

File tree

4 files changed

+152
-26
lines changed

4 files changed

+152
-26
lines changed

packages/openapi-generator/src/codec.ts

Lines changed: 88 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@ import type { SourceFile } from './sourceFile';
1111

1212
import type { KnownCodec } from './knownImports';
1313

14-
type ResolvedRef = { type: 'ref'; name: string; location: string };
15-
16-
type ResolvedIdentifier = ResolvedRef | { type: 'codec'; schema: KnownCodec };
14+
type ResolvedIdentifier = Schema | { type: 'codec'; schema: KnownCodec };
1715

1816
function codecIdentifier(
1917
project: Project,
@@ -57,34 +55,99 @@ function codecIdentifier(
5755
if (object.type !== 'Identifier') {
5856
return E.left(`Unimplemented object type ${object.type}`);
5957
}
60-
const objectSym = source.symbols.imports.find(
58+
59+
// Parse member expressions that come from `* as foo` imports
60+
const starImportSym = source.symbols.imports.find(
6161
(s) => s.localName === object.value && s.type === 'star',
6262
);
63-
if (objectSym === undefined) {
64-
return E.left(`Unknown symbol ${object.value}`);
65-
} else if (id.property.type !== 'Identifier') {
66-
return E.left(`Unimplemented property type ${id.property.type}`);
67-
}
63+
if (starImportSym !== undefined) {
64+
if (id.property.type !== 'Identifier') {
65+
return E.left(`Unimplemented property type ${id.property.type}`);
66+
}
6867

69-
const name = id.property.value;
70-
const knownImport = project.resolveKnownImport(objectSym.from, name);
71-
if (knownImport !== undefined) {
72-
return E.right({ type: 'codec', schema: knownImport });
73-
}
68+
const name = id.property.value;
69+
const knownImport = project.resolveKnownImport(starImportSym.from, name);
70+
if (knownImport !== undefined) {
71+
return E.right({ type: 'codec', schema: knownImport });
72+
}
73+
74+
if (!starImportSym.from.startsWith('.')) {
75+
return E.right({ type: 'ref', name, location: starImportSym.from });
76+
}
77+
78+
const newInitE = findSymbolInitializer(project, source, [
79+
starImportSym.localName,
80+
name,
81+
]);
82+
if (E.isLeft(newInitE)) {
83+
return newInitE;
84+
}
7485

75-
if (!objectSym.from.startsWith('.')) {
76-
return E.right({ type: 'ref', name, location: objectSym.from });
86+
return E.right({ type: 'ref', name, location: newInitE.right[0].path });
7787
}
7888

79-
const newInitE = findSymbolInitializer(project, source, [
80-
objectSym.localName,
81-
name,
82-
]);
83-
if (E.isLeft(newInitE)) {
84-
return newInitE;
89+
// Parse member expressions that come from `import { foo } from 'foo'` imports
90+
const objectImportSym = source.symbols.imports.find(
91+
(s) => s.localName === object.value && s.type === 'named',
92+
);
93+
if (objectImportSym !== undefined) {
94+
if (id.property.type !== 'Identifier') {
95+
return E.left(`Unimplemented property type ${id.property.type}`);
96+
}
97+
const name = id.property.value;
98+
99+
if (!objectImportSym.from.startsWith('.')) {
100+
return E.left(
101+
`Unimplemented named member reference '${objectImportSym.localName}.${name}' from '${objectImportSym.from}'`,
102+
);
103+
}
104+
105+
const newInitE = findSymbolInitializer(project, source, [
106+
objectImportSym.localName,
107+
name,
108+
]);
109+
if (E.isLeft(newInitE)) {
110+
return newInitE;
111+
}
112+
const [newSourceFile, newInit] = newInitE.right;
113+
114+
const objectSchemaE = parsePlainInitializer(project, newSourceFile, newInit);
115+
if (E.isLeft(objectSchemaE)) {
116+
return objectSchemaE;
117+
} else if (objectSchemaE.right.type !== 'object') {
118+
return E.left(`Expected object, got '${objectSchemaE.right.type}'`);
119+
} else if (objectSchemaE.right.properties[name] === undefined) {
120+
return E.left(
121+
`Unknown property '${name}' in '${objectImportSym.localName}' from '${objectImportSym.from}'`,
122+
);
123+
} else {
124+
return E.right(objectSchemaE.right.properties[name]!);
125+
}
85126
}
86127

87-
return E.right({ type: 'ref', name, location: newInitE.right[0].path });
128+
// Parse locally declared member expressions
129+
const declarationSym = source.symbols.declarations.find(
130+
(s) => s.name === object.value,
131+
);
132+
if (declarationSym === undefined) {
133+
return E.left(`Unknown identifier ${object.value}`);
134+
} else if (id.property.type !== 'Identifier') {
135+
return E.left(`Unimplemented property type ${id.property.type}`);
136+
}
137+
const schemaE = parsePlainInitializer(project, source, declarationSym.init);
138+
if (E.isLeft(schemaE)) {
139+
return schemaE;
140+
} else if (schemaE.right.type !== 'object') {
141+
return E.left(
142+
`Expected object, got '${schemaE.right.type}' for '${declarationSym.name}'`,
143+
);
144+
} else if (schemaE.right.properties[id.property.value] === undefined) {
145+
return E.left(
146+
`Unknown property '${id.property.value}' in '${declarationSym.name}'`,
147+
);
148+
} else {
149+
return E.right(schemaE.right.properties[id.property.value]!);
150+
}
88151
} else {
89152
return E.left(`Unimplemented codec type ${id}`);
90153
}
@@ -261,7 +324,7 @@ export function parseCodecInitializer(
261324
}
262325
const identifier = identifierE.right;
263326

264-
if (identifier.type === 'ref') {
327+
if (identifier.type !== 'codec') {
265328
return E.right(identifier);
266329
}
267330

@@ -278,7 +341,7 @@ export function parseCodecInitializer(
278341
}
279342
const identifier = identifierE.right;
280343

281-
if (identifier.type === 'ref') {
344+
if (identifier.type !== 'codec') {
282345
return E.right(identifier);
283346
}
284347

packages/openapi-generator/src/resolveInit.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export function findSymbolInitializer(
6363
return findExportedDeclaration(project, impSourceFile.right, name[1]);
6464
}
6565
}
66-
return E.left(`Unknown identifier ${name[0]}.${name[1]}`);
66+
name = name[0];
6767
}
6868
for (const declaration of sourceFile.symbols.declarations) {
6969
if (declaration.name === name) {

packages/openapi-generator/test/codec.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,3 +719,34 @@ testCase('httpRequest combinator is parsed', HTTP_REQUEST_COMBINATOR, {
719719
required: ['params', 'query'],
720720
},
721721
});
722+
723+
const OBJECT_PROPERTY = `
724+
import * as t from 'io-ts';
725+
726+
const props = {
727+
foo: t.number,
728+
bar: t.string,
729+
};
730+
731+
export const FOO = t.type({
732+
baz: props.foo,
733+
});
734+
`;
735+
736+
testCase('object property is parsed', OBJECT_PROPERTY, {
737+
FOO: {
738+
type: 'object',
739+
properties: {
740+
baz: { type: 'primitive', value: 'number' },
741+
},
742+
required: ['baz'],
743+
},
744+
props: {
745+
type: 'object',
746+
properties: {
747+
foo: { type: 'primitive', value: 'number' },
748+
bar: { type: 'primitive', value: 'string' },
749+
},
750+
required: ['foo', 'bar'],
751+
},
752+
});

packages/openapi-generator/test/resolve.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,3 +515,35 @@ testCase('cross-file star multi export is parsed', STAR_MULTI_EXPORT, '/index.ts
515515
required: ['baz'],
516516
},
517517
});
518+
519+
const IMPORT_MEMBER_EXPRESSION = {
520+
'/foo.ts': `
521+
import * as t from 'io-ts';
522+
export const Foos = {
523+
foo: t.number,
524+
}
525+
`,
526+
'/index.ts': `
527+
import * as t from 'io-ts';
528+
import { Foos } from './foo';
529+
export const FOO = t.type({ foo: Foos.foo });
530+
`,
531+
};
532+
533+
testCase(
534+
'cross-file import member expression is parsed',
535+
IMPORT_MEMBER_EXPRESSION,
536+
'/index.ts',
537+
{
538+
FOO: {
539+
type: 'object',
540+
properties: {
541+
foo: {
542+
type: 'primitive',
543+
value: 'number',
544+
},
545+
},
546+
required: ['foo'],
547+
},
548+
},
549+
);

0 commit comments

Comments
 (0)