Skip to content

Instantly share code, notes, and snippets.

@kazuyaseki
Last active October 10, 2022 14:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kazuyaseki/cb00a0e01a36e4f5898b47000d08b739 to your computer and use it in GitHub Desktop.
Save kazuyaseki/cb00a0e01a36e4f5898b47000d08b739 to your computer and use it in GitHub Desktop.
import * as fs from 'fs';
import * as ts from 'typescript';
type FactoryString = {
factoryString: string;
dependsOn: string[];
typeName: string;
};
type EnumValues = { [key in string]: any };
// aspida の型定義を取得
const typeDefs = fs.readFileSync('./src/api/@types/index.ts', 'utf8');
const outputFilename = `./src/tests/factory/index.ts`;
const sourceFile = ts.createSourceFile(
outputFilename,
typeDefs,
ts.ScriptTarget.Latest
);
// プロパティ名: 型の文字列 なマップと型間の依存関係を作る
type EntityMap = { key: string; type: string }[];
function convertTypeNodeToMap(node: ts.TypeNode): {
map: EntityMap;
dependsOn: string[];
} {
const valuesNode = node.getChildren(sourceFile)[1];
const dependsOn: string[] = [];
const map: EntityMap = valuesNode.getChildren(sourceFile).map((valueNode) => {
const key = valueNode
.getChildren(sourceFile)
.filter((token) => ts.isPropertyName(token))[0]
.getText(sourceFile);
const typeNode = valueNode
.getChildren(sourceFile)
.filter(
(token) =>
ts.isTypeNode(token) ||
ts.isEnumDeclaration(token) ||
ts.isTypeReferenceNode(token) ||
ts.isArrayTypeNode(token)
)[0];
const type = typeNode ? typeNode.getText(sourceFile) : '';
if (isPropertyObject(type)) {
dependsOn.push(type.split('|')[0].trim().replace('[]', ''));
}
return {
key,
type,
};
});
return {
map,
dependsOn,
};
}
function isTypeOf(typeName: string, typeStr: string) {
if (
typeStr === typeName ||
typeStr === `${typeName} | null` ||
typeStr === `${typeName} | undefined` ||
typeStr === `${typeName} | null | undefined`
) {
return true;
}
return false;
}
// 型定義やネーミングルールによってどんなダミーデータを入れるかを指定する
function dummyDataStringByType(typeStr: string, enumValues: EnumValues) {
if (isTypeOf('number', typeStr)) {
return 'faker.datatype.number()';
}
if (isTypeOf('boolean', typeStr)) {
return 'faker.datatype.boolean()';
}
if (isTypeOf('string', typeStr)) {
return 'faker.datatype.string()';
}
if (
['Date | string | null', 'Date | string', 'Date | null', 'Date'].includes(
typeStr
)
) {
return 'faker.datatype.datetime()';
}
if (typeStr) {
if (typeStr.includes('Enum')) {
return enumValues[typeStr];
}
if (typeStr.includes('[]')) {
return '[]';
}
if (typeStr.includes('datetime')) {
return "dayjs().format('YYYY-MM-DD HH:mm')";
}
// 他のオブジェクトの場合は、そのオブジェクトの Factory 関数を呼び出す
if (isPropertyObject(typeStr)) {
return `${typeStr.split('|')[0].trim()}Factory()`;
}
}
// それ以外は大体配列なので空配列を返しておくとうまくいく
return '[]';
}
// Factory ファイルの文字列を生成する関数です。
function generateFactoryFileString(
typeName: string,
typeMap: EntityMap,
enumValues: EnumValues
) {
return `
export const ${typeName}DefaultAttributes: ${typeName} = {
${typeMap
.map((val) => `${val.key}: ${dummyDataStringByType(val.type, enumValues)}`)
.join(',\n ')}
};
export const ${typeName}Factory = (
attirubutes?: Partial<${typeName}>
): ${typeName} => ({
...${typeName}DefaultAttributes,
...attirubutes,
});
`;
}
const isEnum = (name: string) => {
return name.includes('Enum');
};
function isNumeric(value: string) {
return /^-?\d+$/.test(value);
}
function generateFactoryStrings() {
const factoryStrings: FactoryString[] = [];
const enumValues: EnumValues = {};
// TypeAlias それぞれに対して Factory 関数の文字列を生成
ts.forEachChild(sourceFile, (node) => {
if (ts.isTypeAliasDeclaration(node)) {
const typeName = node.name.text;
// Enum (表現上は Union Type)は TypeAlias とは別の扱いにする
if (isEnum(typeName)) {
const enumValue = node
.getChildren(sourceFile)
.filter((node) => ts.isTypeNode(node))
.map((node) => node.getText(sourceFile))[0];
// ダミーデータとしてはとりあえず最初の値を取っておく
const enumValueStr = enumValue.split('|')[0].trim();
enumValues[typeName] = isNumeric(enumValueStr)
? Number(enumValueStr)
: enumValueStr;
}
}
});
ts.forEachChild(sourceFile, (node) => {
if (ts.isTypeAliasDeclaration(node)) {
const typeName = node.name.text;
// Enum (表現上は Union Type)は TypeAlias とは別の扱いにする
if (!isEnum(typeName)) {
const valueNode = (
node
.getChildren(sourceFile)
.filter((node) => ts.isTypeNode(node)) as ts.TypeNode[]
)[0];
const { map, dependsOn } = convertTypeNodeToMap(valueNode);
const factoryString = generateFactoryFileString(
typeName,
map,
enumValues
);
// すでにある場合は追加しない
if (!factoryStrings.find((str) => str.typeName === typeName)) {
factoryStrings.push({ factoryString, dependsOn, typeName });
}
}
}
});
return factoryStrings;
}
// プロパティの型名の先頭が大文字だったら他のオブジェクトだと判定している
const isPropertyObject = (name?: string) => {
if (!name) return false;
const firstLetter = name.charAt(0);
return firstLetter === firstLetter.toUpperCase();
};
function sortFactoryStrings(
factoryString: FactoryString,
sorted: FactoryString[],
factoryStrings: FactoryString[]
) {
if (factoryString.dependsOn.length > 0) {
// 依存しているものがある場合は先に足す
factoryString.dependsOn.forEach((dependedTypeName) => {
const dependedEntityIndex = factoryStrings.findIndex(
(str) => str.typeName === dependedTypeName
);
if (dependedEntityIndex > -1) {
const dependedFactoryString = factoryStrings[dependedEntityIndex];
factoryStrings.splice(dependedEntityIndex, 1);
sortFactoryStrings(dependedFactoryString, sorted, factoryStrings);
}
});
}
sorted.push(factoryString);
}
function main() {
const factoryStrings = generateFactoryStrings();
const sorted: FactoryString[] = [];
while (factoryStrings.length > 0) {
const _factoryString = factoryStrings.shift();
if (_factoryString) {
sortFactoryStrings(_factoryString, sorted, factoryStrings);
}
}
const result = `import { faker } from '@faker-js/faker';
import dayjs from 'dayjs';
import { ${sorted.map((val) => val.typeName).join(', ')} } from "@/api/@types";
${sorted.map((val) => val.factoryString).join('\n')}
`;
fs.writeFileSync(outputFilename, result, 'utf-8');
console.log(`生成に成功しました!${outputFilename} をご確認ください!!`);
}
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment