diff --git a/src/decorators/class.ts b/src/decorators/class.ts new file mode 100644 index 0000000..3ed68d6 --- /dev/null +++ b/src/decorators/class.ts @@ -0,0 +1,15 @@ +import { TypeStorage } from "@root/utils/type-storage"; +import { ClassData } from "@utils/types"; + +type DocumentTypeOptions = ClassData["userData"]; + +export function DocType(options: DocumentTypeOptions): ClassDecorator { + return target => { + TypeStorage.instance.collectClassData({ + classType: target, + userData: { + ...options, + }, + }); + }; +} diff --git a/src/decorators/field.ts b/src/decorators/field.ts new file mode 100644 index 0000000..f48f6f5 --- /dev/null +++ b/src/decorators/field.ts @@ -0,0 +1,33 @@ +import { DecoratorType, FieldData } from "@utils/types"; +import { TypeStorage } from "@root/utils/type-storage"; + +type DocumentFieldOptions = FieldData["userData"]; + +export function DocField(fieldData: DocumentFieldOptions): DecoratorType { + return (target: object, propertyKey: string | symbol, descriptor?: TypedPropertyDescriptor) => { + if (typeof propertyKey === "symbol") { + throw new Error("Symbol keys are not supported yet!"); + } + + const isMethod = Boolean(descriptor && descriptor.value); + if (isMethod) { + throw new Error("Field decorator can only be used on properties!"); + } + + const type = Reflect.getMetadata("design:type", target, propertyKey); + if (!type || (type !== Number && type !== String && type !== Boolean)) { + throw new Error("Field decorator can only be used on properties of type Number, String or Boolean!"); + } + + TypeStorage.instance.collectFieldData({ + type, + fieldName: propertyKey, + classType: target.constructor, + userData: { + ...fieldData, + description: fieldData.description || `field '${propertyKey}'.`, + nullable: fieldData.nullable ?? false, + }, + }); + }; +} diff --git a/src/decorators/index.ts b/src/decorators/index.ts new file mode 100644 index 0000000..8bfda66 --- /dev/null +++ b/src/decorators/index.ts @@ -0,0 +1,2 @@ +export * from "./class"; +export * from "./field"; diff --git a/src/generators/class.ts b/src/generators/class.ts new file mode 100644 index 0000000..59febb2 --- /dev/null +++ b/src/generators/class.ts @@ -0,0 +1,31 @@ +import { ClassType } from "@utils/types"; +import { TypeStorage } from "@root/utils/type-storage"; + +import { generateTableFromField, getHeaderGenerator } from "@generators/utils"; + +interface DocumentOptions { + initialHeaderLevel?: number; // = 1 +} + +export function generateDocsForClass(classType: ClassType, options?: DocumentOptions) { + const { initialHeaderLevel = 1 } = options || {}; + const header = getHeaderGenerator(initialHeaderLevel); + + const targetClass = TypeStorage.instance.classes.find(classData => classData.classType === classType); + if (!targetClass) { + throw new Error(`Class '${classType.name}' is not registered!`); + } + + const contents: string[] = []; + contents.push(`${header(targetClass.userData.name, 1)} (${targetClass.className})\n`); + contents.push(`${targetClass.userData.description}\n`); + contents.push(`${header("Fields", 2)}\n`); + + for (const field of targetClass.fields) { + contents.push(`${header(`\`${field.fieldName}\``, 3)}\n`); + contents.push(generateTableFromField(field)); + contents.push("\n\n"); + } + + return contents.join("\n"); +} diff --git a/src/generators/index.ts b/src/generators/index.ts new file mode 100644 index 0000000..936ab95 --- /dev/null +++ b/src/generators/index.ts @@ -0,0 +1 @@ +export * from "./class"; diff --git a/src/generators/utils.ts b/src/generators/utils.ts new file mode 100644 index 0000000..090185a --- /dev/null +++ b/src/generators/utils.ts @@ -0,0 +1,23 @@ +import markdownTable from "markdown-table"; + +import { FieldData } from "@utils/types"; + +export function getHeaderGenerator(initialLevel = 1) { + return function header(text: string, level: number) { + return `${"#".repeat(level + (initialLevel - 1))} ${text}`; + }; +} + +export function generateTableFromField(field: FieldData) { + const rows: string[][] = []; + + rows.push(["Name", "Description"]); + rows.push(["Type", field.type.name]); + rows.push(["Nullable", field.userData.nullable ? "✔️ Yes" : "❌ No"]); + + if (field.userData.description) { + rows.push(["Description", field.userData.description]); + } + + return markdownTable(rows); +} diff --git a/src/index.ts b/src/index.ts index e69de29..371db33 100644 --- a/src/index.ts +++ b/src/index.ts @@ -0,0 +1,4 @@ +import type * as _ from "reflect-metadata"; + +export * from "./decorators"; +export * from "./generators"; diff --git a/src/utils/type-storage.ts b/src/utils/type-storage.ts new file mode 100644 index 0000000..ccb4e94 --- /dev/null +++ b/src/utils/type-storage.ts @@ -0,0 +1,63 @@ +import { ClassData, ClassType, FieldData } from "@utils/types"; + +interface CollectFieldDataOptions extends FieldData { + classType: ClassType; +} + +interface CollectClassDataOptions extends Omit { + classType: ClassType; +} + +export class TypeStorage { + public static readonly instance = new TypeStorage(); + + private constructor(private readonly classMap = new Map()) {} + + public get classes() { + return [...this.classMap.values()]; + } + + public collectFieldData(fieldData: CollectFieldDataOptions) { + const classData = this.getClassData(fieldData.classType); + + classData.fields.push({ + type: fieldData.type, + fieldName: fieldData.fieldName, + userData: { + ...fieldData.userData, + }, + }); + } + public collectClassData({ classType, userData }: CollectClassDataOptions) { + const classData = this.getClassData(classType); + classData.className = classType.name; + classData.classType = classType; + classData.userData = { + ...userData, + }; + } + + private getClassData(classType: ClassType) { + if (!this.classMap.has(classType)) { + const classData: ClassData = { + classType, + className: classType.name, + fields: [], + userData: { + name: classType.name, + description: `type '${classType.name}'.`, + }, + }; + this.classMap.set(classType, classData); + + return classData; + } + + const classData = this.classMap.get(classType); + if (!classData) { + throw new Error("Class data is undefined!"); + } + + return classData; + } +} diff --git a/src/utils/types.ts b/src/utils/types.ts new file mode 100644 index 0000000..2c436da --- /dev/null +++ b/src/utils/types.ts @@ -0,0 +1,25 @@ +export type DecoratorType = PropertyDecorator & MethodDecorator; +export type ClassType = Function; + +export interface FieldData { + type: typeof String | typeof Number | typeof Boolean; + fieldName: string; + + // user defined + userData: { + description?: string; + nullable?: boolean; + defaultValue?: any; + }; +} +export interface ClassData { + className: string; + classType: ClassType; + fields: FieldData[]; + + // user defined + userData: { + name: string; + description: string; + }; +}