diff --git a/packages/openapi-generator/src/codec.ts b/packages/openapi-generator/src/codec.ts index 6f49dc6e..71bc5598 100644 --- a/packages/openapi-generator/src/codec.ts +++ b/packages/openapi-generator/src/codec.ts @@ -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, @@ -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}`); } @@ -261,7 +324,7 @@ export function parseCodecInitializer( } const identifier = identifierE.right; - if (identifier.type === 'ref') { + if (identifier.type !== 'codec') { return E.right(identifier); } @@ -278,7 +341,7 @@ export function parseCodecInitializer( } const identifier = identifierE.right; - if (identifier.type === 'ref') { + if (identifier.type !== 'codec') { return E.right(identifier); } diff --git a/packages/openapi-generator/src/resolveInit.ts b/packages/openapi-generator/src/resolveInit.ts index 7d73590a..26203b49 100644 --- a/packages/openapi-generator/src/resolveInit.ts +++ b/packages/openapi-generator/src/resolveInit.ts @@ -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) { diff --git a/packages/openapi-generator/test/codec.test.ts b/packages/openapi-generator/test/codec.test.ts index d57ea6ba..e2ff3158 100644 --- a/packages/openapi-generator/test/codec.test.ts +++ b/packages/openapi-generator/test/codec.test.ts @@ -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'], + }, +}); diff --git a/packages/openapi-generator/test/resolve.test.ts b/packages/openapi-generator/test/resolve.test.ts index 0a4a79a3..820a742b 100644 --- a/packages/openapi-generator/test/resolve.test.ts +++ b/packages/openapi-generator/test/resolve.test.ts @@ -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'], + }, + }, +);