Skip to content

Commit

Permalink
feat: convert interfaces
Browse files Browse the repository at this point in the history
  • Loading branch information
jedwards1211 committed Feb 16, 2021
1 parent 8d43509 commit 9ac4403
Show file tree
Hide file tree
Showing 10 changed files with 371 additions and 52 deletions.
107 changes: 66 additions & 41 deletions src/convert/ConversionContext.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import * as t from '@babel/types'
import template from '@babel/template'
import traverse from '@babel/traverse'
import convertTSRecordType, { isTSRecordType } from './TSRecordType'
import convertObjectTypeAnnotation from './ObjectTypeAnnotation'
import convertTSRecordType, { isTSRecordType } from './convertTSRecordType'
import convertObjectTypeAnnotation from './convertObjectTypeAnnotation'
import convertInterfaceDeclaration from './convertInterfaceDeclaration'
import NodeConversionError from '../NodeConversionError'
import convertTSTypeLiteral from './TSTypeLiteral'
import convertTSTypeLiteral from './convertTSTypeLiteralOrInterfaceBody'
import { NodePath } from '@babel/traverse'
import { builtinClasses } from './builtinClasses'
import { TSBindingVisitors } from '../ts/TSBindingVisitors'
Expand All @@ -18,6 +19,8 @@ import convertUtilityFlowType from './convertUtilityFlowType'
import moveLeadingCommentsToNextSibling from './moveCommentsToNextSibling'
import areASTsEqual from '../util/areASTsEqual'

import convertTSInterfaceDeclaration from './convertTSInterfaceDeclaration'

const templates = {
importTypedValidators: template.statement`import * as T from 'typed-validators'`,
importValidation: template.statement`import { Validation as LOCAL } from 'typed-validators'`,
Expand Down Expand Up @@ -459,55 +462,51 @@ export class FileConversionContext {
}
case 'ClassDeclaration':
return { converted: type.id, kind: 'class' }
case 'TypeAlias': {
const { id } = type as t.TypeAlias
const T = await this.importT()
const validatorId = this.getValidatorIdentifier(id)
const validatorIdWithType = this.getValidatorIdentifier(id)
validatorIdWithType.typeAnnotation = t.typeAnnotation(
t.genericTypeAnnotation(
t.qualifiedTypeIdentifier(t.identifier('TypeAlias'), T),
t.typeParameterInstantiation([t.genericTypeAnnotation(id)])
case 'InterfaceDeclaration':
case 'TSInterfaceDeclaration':
case 'TypeAlias':
case 'TSTypeAliasDeclaration': {
const casePath = path as
| NodePath<t.TypeAlias>
| NodePath<t.InterfaceDeclaration>
| NodePath<t.TSTypeAliasDeclaration>

const typeParameters: NodePath<any> = path.get('typeParameters') as any
if (typeParameters?.node)
throw new NodeConversionError(
'parameterized types are not supported',
this.file,
typeParameters
)
)
const validator: t.VariableDeclaration = templates.alias({
T,
ID: validatorIdWithType,
NAME: t.stringLiteral(id.name),
TYPE: await this.convert(
(path as NodePath<t.TypeAlias>).get('right')
),
}) as any

const binding = path.scope.getBinding(validatorId.name)
if (binding && binding.path.isVariableDeclarator()) {
binding.path.replaceWith(validator.declarations[0])
} else {
const { parentPath } = path
if (parentPath.isExportNamedDeclaration())
parentPath.insertAfter(t.exportNamedDeclaration(validator))
else path.insertAfter(validator)
}
return { converted: validatorId, kind: 'alias' }
}
case 'TSTypeAliasDeclaration': {
const { id } = type as t.TSTypeAliasDeclaration
const { id } = casePath.node
const T = await this.importT()
const validatorId = this.getValidatorIdentifier(id)
const validatorIdWithType = this.getValidatorIdentifier(id)
validatorIdWithType.typeAnnotation = t.tsTypeAnnotation(
t.tsTypeReference(
t.tsQualifiedName(T, t.identifier('TypeAlias')),
t.tsTypeParameterInstantiation([t.tsTypeReference(id)])
)
)
validatorIdWithType.typeAnnotation = path.isFlow()
? t.typeAnnotation(
t.genericTypeAnnotation(
t.qualifiedTypeIdentifier(t.identifier('TypeAlias'), T),
t.typeParameterInstantiation([t.genericTypeAnnotation(id)])
)
)
: t.tsTypeAnnotation(
t.tsTypeReference(
t.tsQualifiedName(T, t.identifier('TypeAlias')),
t.tsTypeParameterInstantiation([t.tsTypeReference(id)])
)
)

const validator: t.VariableDeclaration = templates.alias({
T,
ID: validatorIdWithType,
NAME: t.stringLiteral(id.name),
TYPE: await this.convert(
(path as NodePath<t.TSTypeAliasDeclaration>).get('typeAnnotation')
casePath.isTypeAlias()
? casePath.get('right')
: casePath.isTSTypeAliasDeclaration()
? casePath.get('typeAnnotation')
: casePath
),
}) as any

Expand Down Expand Up @@ -776,11 +775,21 @@ export class FileConversionContext {
this,
path as NodePath<t.ObjectTypeAnnotation>
)
case 'InterfaceDeclaration':
return await convertInterfaceDeclaration(
this,
path as NodePath<t.InterfaceDeclaration>
)
case 'TSTypeLiteral':
return await convertTSTypeLiteral(
this,
path as NodePath<t.TSTypeLiteral>
)
case 'TSInterfaceDeclaration':
return await convertTSInterfaceDeclaration(
this,
path as NodePath<t.TSInterfaceDeclaration>
)
case 'GenericTypeAnnotation': {
const convertedUtility = await convertUtilityFlowType(this, path)
if (convertedUtility) return convertedUtility
Expand All @@ -790,6 +799,22 @@ export class FileConversionContext {
)
)
}
case 'InterfaceExtends': {
return await this.convertTypeReferenceToValidator(
await this.convertTypeReference(
(path as NodePath<t.InterfaceExtends>).get('id')
)
)
}
case 'TSExpressionWithTypeArguments': {
return await this.convertTypeReferenceToValidator(
await this.convertTypeReference(
(path as NodePath<t.TSExpressionWithTypeArguments>).get(
'expression'
)
)
)
}
case 'TSTypeReference': {
if (isTSRecordType(path)) return await convertTSRecordType(this, path)
return await this.convertTypeReferenceToValidator(
Expand Down
35 changes: 35 additions & 0 deletions src/convert/convertInterfaceDeclaration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { NodePath } from '@babel/traverse'
import * as t from '@babel/types'
import NodeConversionError from '../NodeConversionError'
import { FileConversionContext } from './ConversionContext'
import convertObjectTypeAnnotation from './convertObjectTypeAnnotation'
import template from '@babel/template'

const templates = {
mergeInexact: template.expression(`%%T%%.mergeInexact(%%OBJECTS%%)`),
}

export default async function convertInterfaceDeclaration(
context: FileConversionContext,
path: NodePath<t.InterfaceDeclaration>
): Promise<t.Expression> {
if (path.node.mixins?.length)
throw new NodeConversionError(
'interface mixins are not supported',
context.file,
path
)
const extended = [...path.get('implements'), ...path.get('extends')]
const convertedBody = await convertObjectTypeAnnotation(
context,
path.get('body')
)
if (!extended.length) return convertedBody
return templates.mergeInexact({
T: await context.importT(),
OBJECTS: await Promise.all([
...extended.map((path) => context.convert(path)),
convertedBody,
]),
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ export default async function convertObjectTypeAnnotation(
if (prop.isObjectTypeSpreadProperty()) spreads.push(prop)
}
const indexers = path.get('indexers') as NodePath<t.ObjectTypeIndexer>[]
const exact = !obj.exact && !obj.inexact ? context.defaultExact : obj.exact
const exact =
!path.parentPath.isInterfaceDeclaration() && !obj.exact && !obj.inexact
? context.defaultExact
: obj.exact
if (properties.length === 0 && indexers?.length === 1) {
const [indexer] = indexers
return templates.record({
Expand Down
30 changes: 30 additions & 0 deletions src/convert/convertTSInterfaceDeclaration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { NodePath } from '@babel/traverse'
import * as t from '@babel/types'
import { FileConversionContext } from './ConversionContext'
import convertTSTypeLiteralOrInterfaceBody from './convertTSTypeLiteralOrInterfaceBody'
import template from '@babel/template'

const templates = {
merge: template.expression(`%%T%%.merge(%%OBJECTS%%)`),
}

export default async function convertTSInterfaceDeclaration(
context: FileConversionContext,
path: NodePath<t.TSInterfaceDeclaration>
): Promise<t.Expression> {
const extended: NodePath<t.TSExpressionWithTypeArguments>[] | null = path.get(
'extends'
) as any
const convertedBody = await convertTSTypeLiteralOrInterfaceBody(
context,
path.get('body')
)
if (!extended?.length) return convertedBody
return templates.merge({
T: await context.importT(),
OBJECTS: await Promise.all([
...extended.map((path) => context.convert(path)),
convertedBody,
]),
})
}
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ const templates = {
object: template.expression`T.object(PROPS)`,
}

export default async function convertTSTypeLiteral(
export default async function convertTSTypeLiteralOrInterfaceBody(
context: FileConversionContext,
path: NodePath<t.TSTypeLiteral>
path: NodePath<t.TSTypeLiteral> | NodePath<t.TSInterfaceBody>
): Promise<t.Expression> {
const required: t.ObjectProperty[] = []
const optional: t.ObjectProperty[] = []
for (const _property of path.get('members')) {
for (const _property of path.isTSTypeLiteral()
? path.get('members')
: path.get('body')) {
if (!_property.isTSPropertySignature()) {
throw new NodeConversionError(
`Unsupported object property`,
Expand Down
20 changes: 13 additions & 7 deletions src/ts/TSBindingVisitors.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { NodePath } from '@babel/traverse'
import * as t from '@babel/types'

function registerIdBinding(
path: NodePath<t.TSTypeAliasDeclaration> | NodePath<t.TSInterfaceDeclaration>
): void {
path.scope.registerBinding(
'type',
path.get('id') as NodePath<any>,
path as NodePath<any>
)
}

export const TSBindingVisitors: {
TSTypeAliasDeclaration(path: NodePath<t.TSTypeAliasDeclaration>): void
TSInterfaceDeclaration(path: NodePath<t.TSInterfaceDeclaration>): void
} = {
// work around @babel/traverse bug
TSTypeAliasDeclaration(path: NodePath<t.TSTypeAliasDeclaration>) {
path.scope.registerBinding(
'type',
path.get('id') as NodePath<any>,
path as NodePath<any>
)
},
TSTypeAliasDeclaration: registerIdBinding,
TSInterfaceDeclaration: registerIdBinding,
}
25 changes: 25 additions & 0 deletions temp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as t from 'typed-validators'

type Blah = {
bar: number
}

const BlahType: t.TypeAlias<Blah> = t.alias(
'Blah',
t.object({
bar: t.number(),
})
)

type Test = {
foo: string
blah: Blah
}

const TestType: t.TypeAlias<Test> = t.alias(
'Test',
t.object({
foo: t.string(),
blah: t.ref(() => BlahType),
})
)
Loading

0 comments on commit 9ac4403

Please sign in to comment.