Dark magic of code transformation
Valeriy Kuzmin, Lilt, 2021
Problem: DTO type sharing
#[derive(TypescriptDefinition, TypeScriptify,
Deserialize, Serialize)]
#[serde(tag = "tag")]
pub enum InventoryAction {
Unknown,
Split { from: Uuid, count: i32 },
Merge { from: Uuid, to: Uuid },
Move { item: Uuid, index: i32 },
}
Rust
Problem: DTO type sharing
export type InventoryActionUnknown = { tag: "Unknown" };
export type InventoryActionSplit = {
tag: "Split",
from: string,
count: number,
};
// more variants...
export type InventoryAction = InventoryActionUnknown | InventoryActionSplit;
export class InventoryActionBuilder {
static InventoryActionUnknown = (): InventoryActionUnknown => ({
tag: "Unknown"
});
static InventoryActionSplit = ({
from,
count
}): InventoryActionSplit => ({
tag: "Split",
from,
count
});
// more variants...
}
Typescript
Problem: DTO type sharing
export type InventoryAction =
| { tag: 'Unknown' }
| { tag: 'Split'; from: Uuid; count: number }
| { tag: 'Merge'; from: Uuid; to: number }
| { tag: 'Move'; item: Uuid; index: number };
typescript-definitions crate
Task 1: Advanced unions
export type InventoryAction = smth;
ExportNamedDeclaration with TSTypeAliasDeclaration
.replaceWith((ex: ASTPath<ExportNamedDeclaration>) => {
if (ex.value.declaration.type !== 'TSTypeAliasDeclaration') {
return ex.value;
}
// ...
}
Task 1: Advanced unions
... type InventoryAction = Elem1 | Elem2;
type id = Identifier + type annotation of TSUnionType
// ...
// separate declaration from export
const p = ex.value.declaration;
// returns p.id.typeName if p.id.type === 'Identifier'
const mainUnionName = getTypeDeclaraiontName(p);
if (!mainUnionName) {
return ex.value;
}
// extract the right part of the assignment
const union = p.typeAnnotation;
if (!isTSUnionType(union)) {
// not a union, not interesting
return ex.value;
}
// ...
Task 1: Advanced unions
type C = {tag: "A"} | {tag: "B"};
union.types ➟ [['CA','A'], ['CB', 'B']] ➟ Record<Tag, ExtractedTypeName>
union.types ➟ [['A', <A type obj>], ['B', <B type obj>]] ➟ Record<Tag, ExtractedType>
type CA = {tag: "A"};
type CB = {tag: "B"};
type C = CA | CB;
Task 1: Advanced unions
{tag: "A", foo: "bar"}
type CA = {tag: "A", foo: "bar"};
TSTypeLiteral
TSPropertySignature[]
key.type = 'Identifier'
valuer.type = 'Identifier' | 'TSNumberKeyWord' | 'TSStringKeyword' | 'TSLiteralType'
ObjectTypeAnnotation
Task 2: Builder classes
export type C = CA | CB;
ExportNamedDeclaration with type = 'TSTypeAliasDeclaration'
.find(j.ExportNamedDeclaration)
.insertAfter((ex: ASTPath<ExportNamedDeclaration>) => {
if (ex.value.declaration.type !== 'TSTypeAliasDeclaration') {
return [];
}
const union = ex.value.declaration.typeAnnotation;
// ...
Task 2: Builder classes
return j.exportNamedDeclaration(
j.classDeclaration.from({
id: j.identifier(builderClassName),
body: j.classBody(
union.types
.map((subType) => {
if (!isTsTypeReference(subType)) {
return null;
}
if (!isIdentifier(subType.typeName)) {
return null;
}
return j.classProperty.from({
key: j.identifier(subType.typeName.name),
static: true,
accessibility: 'public',
declare: false,
value: makeBuilderMethod(
subType.typeName.name,
typeMap,
j,
mainTypeName
),
});
})
.filter((t) => !!t)
),
})
)
The end!
Questions?
jscodeshift-2-at-lilt
By Valeriy Kuzmin
jscodeshift-2-at-lilt
- 197