-
-
Save kazuyaseki/cb00a0e01a36e4f5898b47000d08b739 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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