Skip to content

Commit

Permalink
feat: implement basic document generation feature
Browse files Browse the repository at this point in the history
  • Loading branch information
async3619 committed Dec 8, 2022
1 parent 9f379d2 commit 8e528d4
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 0 deletions.
15 changes: 15 additions & 0 deletions src/decorators/class.ts
Original file line number Diff line number Diff line change
@@ -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,
},
});
};
}
33 changes: 33 additions & 0 deletions src/decorators/field.ts
Original file line number Diff line number Diff line change
@@ -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 <T>(target: object, propertyKey: string | symbol, descriptor?: TypedPropertyDescriptor<T>) => {
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,
},
});
};
}
2 changes: 2 additions & 0 deletions src/decorators/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./class";
export * from "./field";
31 changes: 31 additions & 0 deletions src/generators/class.ts
Original file line number Diff line number Diff line change
@@ -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");
}
1 change: 1 addition & 0 deletions src/generators/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./class";
23 changes: 23 additions & 0 deletions src/generators/utils.ts
Original file line number Diff line number Diff line change
@@ -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);
}
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type * as _ from "reflect-metadata";

export * from "./decorators";
export * from "./generators";
63 changes: 63 additions & 0 deletions src/utils/type-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { ClassData, ClassType, FieldData } from "@utils/types";

interface CollectFieldDataOptions extends FieldData {
classType: ClassType;
}

interface CollectClassDataOptions extends Omit<ClassData, "className" | "fields"> {
classType: ClassType;
}

export class TypeStorage {
public static readonly instance = new TypeStorage();

private constructor(private readonly classMap = new Map<ClassType, ClassData>()) {}

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;
}
}
25 changes: 25 additions & 0 deletions src/utils/types.ts
Original file line number Diff line number Diff line change
@@ -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;
};
}

0 comments on commit 8e528d4

Please sign in to comment.