Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 88 additions & 25 deletions packages/openapi-generator/src/codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ import type { SourceFile } from './sourceFile';

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

type ResolvedRef = { type: 'ref'; name: string; location: string };

type ResolvedIdentifier = ResolvedRef | { type: 'codec'; schema: KnownCodec };
type ResolvedIdentifier = Schema | { type: 'codec'; schema: KnownCodec };

function codecIdentifier(
project: Project,
Expand Down Expand Up @@ -57,34 +55,99 @@ function codecIdentifier(
if (object.type !== 'Identifier') {
return E.left(`Unimplemented object type ${object.type}`);
}
const objectSym = source.symbols.imports.find(

// Parse member expressions that come from `* as foo` imports
const starImportSym = source.symbols.imports.find(
(s) => s.localName === object.value && s.type === 'star',
);
if (objectSym === undefined) {
return E.left(`Unknown symbol ${object.value}`);
} else if (id.property.type !== 'Identifier') {
return E.left(`Unimplemented property type ${id.property.type}`);
}
if (starImportSym !== undefined) {
if (id.property.type !== 'Identifier') {
return E.left(`Unimplemented property type ${id.property.type}`);
}

const name = id.property.value;
const knownImport = project.resolveKnownImport(objectSym.from, name);
if (knownImport !== undefined) {
return E.right({ type: 'codec', schema: knownImport });
}
const name = id.property.value;
const knownImport = project.resolveKnownImport(starImportSym.from, name);
if (knownImport !== undefined) {
return E.right({ type: 'codec', schema: knownImport });
}

if (!starImportSym.from.startsWith('.')) {
return E.right({ type: 'ref', name, location: starImportSym.from });
}

const newInitE = findSymbolInitializer(project, source, [
starImportSym.localName,
name,
]);
if (E.isLeft(newInitE)) {
return newInitE;
}

if (!objectSym.from.startsWith('.')) {
return E.right({ type: 'ref', name, location: objectSym.from });
return E.right({ type: 'ref', name, location: newInitE.right[0].path });
}

const newInitE = findSymbolInitializer(project, source, [
objectSym.localName,
name,
]);
if (E.isLeft(newInitE)) {
return newInitE;
// Parse member expressions that come from `import { foo } from 'foo'` imports
const objectImportSym = source.symbols.imports.find(
(s) => s.localName === object.value && s.type === 'named',
);
if (objectImportSym !== undefined) {
if (id.property.type !== 'Identifier') {
return E.left(`Unimplemented property type ${id.property.type}`);
}
const name = id.property.value;

if (!objectImportSym.from.startsWith('.')) {
return E.left(
`Unimplemented named member reference '${objectImportSym.localName}.${name}' from '${objectImportSym.from}'`,
);
}

const newInitE = findSymbolInitializer(project, source, [
objectImportSym.localName,
name,
]);
if (E.isLeft(newInitE)) {
return newInitE;
}
const [newSourceFile, newInit] = newInitE.right;

const objectSchemaE = parsePlainInitializer(project, newSourceFile, newInit);
if (E.isLeft(objectSchemaE)) {
return objectSchemaE;
} else if (objectSchemaE.right.type !== 'object') {
return E.left(`Expected object, got '${objectSchemaE.right.type}'`);
} else if (objectSchemaE.right.properties[name] === undefined) {
return E.left(
`Unknown property '${name}' in '${objectImportSym.localName}' from '${objectImportSym.from}'`,
);
} else {
return E.right(objectSchemaE.right.properties[name]!);
}
}

return E.right({ type: 'ref', name, location: newInitE.right[0].path });
// Parse locally declared member expressions
const declarationSym = source.symbols.declarations.find(
(s) => s.name === object.value,
);
if (declarationSym === undefined) {
return E.left(`Unknown identifier ${object.value}`);
} else if (id.property.type !== 'Identifier') {
return E.left(`Unimplemented property type ${id.property.type}`);
}
const schemaE = parsePlainInitializer(project, source, declarationSym.init);
if (E.isLeft(schemaE)) {
return schemaE;
} else if (schemaE.right.type !== 'object') {
return E.left(
`Expected object, got '${schemaE.right.type}' for '${declarationSym.name}'`,
);
} else if (schemaE.right.properties[id.property.value] === undefined) {
return E.left(
`Unknown property '${id.property.value}' in '${declarationSym.name}'`,
);
} else {
return E.right(schemaE.right.properties[id.property.value]!);
}
} else {
return E.left(`Unimplemented codec type ${id}`);
}
Expand Down Expand Up @@ -261,7 +324,7 @@ export function parseCodecInitializer(
}
const identifier = identifierE.right;

if (identifier.type === 'ref') {
if (identifier.type !== 'codec') {
return E.right(identifier);
}

Expand All @@ -278,7 +341,7 @@ export function parseCodecInitializer(
}
const identifier = identifierE.right;

if (identifier.type === 'ref') {
if (identifier.type !== 'codec') {
return E.right(identifier);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/openapi-generator/src/resolveInit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export function findSymbolInitializer(
return findExportedDeclaration(project, impSourceFile.right, name[1]);
}
}
return E.left(`Unknown identifier ${name[0]}.${name[1]}`);
name = name[0];
}
for (const declaration of sourceFile.symbols.declarations) {
if (declaration.name === name) {
Expand Down
31 changes: 31 additions & 0 deletions packages/openapi-generator/test/codec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -719,3 +719,34 @@ testCase('httpRequest combinator is parsed', HTTP_REQUEST_COMBINATOR, {
required: ['params', 'query'],
},
});

const OBJECT_PROPERTY = `
import * as t from 'io-ts';

const props = {
foo: t.number,
bar: t.string,
};

export const FOO = t.type({
baz: props.foo,
});
`;

testCase('object property is parsed', OBJECT_PROPERTY, {
FOO: {
type: 'object',
properties: {
baz: { type: 'primitive', value: 'number' },
},
required: ['baz'],
},
props: {
type: 'object',
properties: {
foo: { type: 'primitive', value: 'number' },
bar: { type: 'primitive', value: 'string' },
},
required: ['foo', 'bar'],
},
});
32 changes: 32 additions & 0 deletions packages/openapi-generator/test/resolve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,3 +515,35 @@ testCase('cross-file star multi export is parsed', STAR_MULTI_EXPORT, '/index.ts
required: ['baz'],
},
});

const IMPORT_MEMBER_EXPRESSION = {
'/foo.ts': `
import * as t from 'io-ts';
export const Foos = {
foo: t.number,
}
`,
'/index.ts': `
import * as t from 'io-ts';
import { Foos } from './foo';
export const FOO = t.type({ foo: Foos.foo });
`,
};

testCase(
'cross-file import member expression is parsed',
IMPORT_MEMBER_EXPRESSION,
'/index.ts',
{
FOO: {
type: 'object',
properties: {
foo: {
type: 'primitive',
value: 'number',
},
},
required: ['foo'],
},
},
);