Skip to content

Commit

Permalink
feat: add support for nested type
Browse files Browse the repository at this point in the history
  • Loading branch information
async3619 committed Dec 9, 2022
1 parent f410907 commit ec3fbc8
Show file tree
Hide file tree
Showing 10 changed files with 178 additions and 23 deletions.
13 changes: 12 additions & 1 deletion src/decorators/field.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,17 @@ describe("@DocField() decorator", function () {
});
});

it("should throw an error if the field has unregistered type", function () {
expect(() => {
class UnregisteredType {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
class MockedClass {
@DocField({})
public test!: UnregisteredType;
}
}).toThrowError("Type 'UnregisteredType' is not registered.");
});

it("should throw an error if the field is a method", function () {
expect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand Down Expand Up @@ -140,7 +151,7 @@ describe("@DocField() decorator", function () {
}).toThrowError("Symbol keys are not supported yet!");
});

it("should throw an error if the field does not have supported types", () => {
it("should throw an error if the field has not supported types", () => {
expect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
class MockedClass {
Expand Down
5 changes: 3 additions & 2 deletions src/decorators/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ export function DocField(fieldData: DocumentFieldOptions): DecoratorType {

const type = Reflect.getMetadata("design:type", target, propertyKey);
const desiredType = fieldData.type?.();
const targetType = checkType(type, desiredType, className, propertyKey);
const [targetType, isArray, isCustom] = checkType(type, desiredType, className, propertyKey);

getTypeStorage().collectFieldData({
classType: target.constructor,
field: {
type: targetType,
fieldName: propertyKey,
isArray: type === Array,
isArray,
isCustom,
userData: {
...fieldData,
},
Expand Down
54 changes: 53 additions & 1 deletion src/generators/__snapshots__/class.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`generateDocsForClass() Function should generate documentation correctly 1`] = `
"# MockedClass (MockedClass)
"# Type \`MockedClass\`
type 'MockedClass'.
Expand Down Expand Up @@ -30,5 +30,57 @@ type 'MockedClass'.
| Description | test |
"
`;

exports[`generateDocsForClass() Function should generate documentation correctly with nested fields 1`] = `
"# Type \`MockedClass2\`
type 'MockedClass2'.
## Fields
- [test](#test)
### \`test\`
| Name | Description |
| ----------- | -------------------------------- |
| Type | [MockedClass](#type-mockedclass) |
| Nullable | ❌ No |
| Description | test |
## Type \`MockedClass\`
type 'MockedClass'.
### Fields
- [test](#test)
- [test2](#test2)
#### \`test\`
| Name | Description |
| ----------- | ----------- |
| Type | Boolean |
| Nullable | ❌ No |
| Description | test |
#### \`test2\`
| Name | Description |
| ----------- | ----------- |
| Type | Boolean |
| Nullable | ✔️ Yes |
| Description | test |
"
`;
30 changes: 30 additions & 0 deletions src/generators/class.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,36 @@ describe("generateDocsForClass() Function", function () {
expect(generatedData).toMatchSnapshot();
});

it("should generate documentation correctly with nested fields", function () {
class MockedClass {
@DocField({
description: "test",
nullable: false,
})
public test!: boolean;

@DocField({
description: "test",
nullable: true,
})
public test2!: boolean;
}

class MockedClass2 {
@DocField({
description: "test",
nullable: false,
})
public test!: MockedClass;
}

const generatedData = generateForClass(MockedClass2, {
combineNestedFields: true,
});

expect(generatedData).toMatchSnapshot();
});

it("should throw an error if the class is not registered", function () {
expect(() => {
generateForClass(String);
Expand Down
19 changes: 16 additions & 3 deletions src/generators/class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import { generateTableFromField } from "@utils/generate-table";
interface DocumentOptions {
initialHeaderLevel?: number; // = 1
withFieldTOC?: boolean; // = true
combineNestedFields?: boolean; // = false
}

export function generateForClass(classType: ClassType, options?: DocumentOptions) {
const { initialHeaderLevel = 1, withFieldTOC = true } = options || {};
const { initialHeaderLevel = 1, withFieldTOC = true, combineNestedFields = false } = options || {};
const header = getHeaderGenerator(initialHeaderLevel);

const targetClass = getTypeStorage().classes.find(classData => classData.classType === classType);
Expand All @@ -19,7 +20,7 @@ export function generateForClass(classType: ClassType, options?: DocumentOptions
}

const contents: string[] = [];
contents.push(`${header(targetClass.userData.name, 1)} (${targetClass.className})\n`);
contents.push(`${header(`Type \`${targetClass.userData.name}\``, 1)}\n`);
contents.push(`${targetClass.userData.description}\n`);
contents.push(`${header("Fields", 2)}\n`);

Expand All @@ -34,9 +35,21 @@ export function generateForClass(classType: ClassType, options?: DocumentOptions

for (const field of fields) {
contents.push(`${header(`\`${field.fieldName}\``, 3)}\n`);
contents.push(generateTableFromField(field));
contents.push(generateTableFromField(field, combineNestedFields));
contents.push("\n\n");
}

if (combineNestedFields) {
const customClasses = fields.filter(f => f.isCustom);
for (const customClass of customClasses) {
contents.push(
...generateForClass(customClass.type, {
...options,
initialHeaderLevel: Math.min(initialHeaderLevel + 1, 6),
}).split("\n"),
);
}
}

return contents.join("\n");
}
29 changes: 15 additions & 14 deletions src/utils/check-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,18 @@ export function checkType(
userType: AvailableTypes | undefined,
className: string,
propertyKey: string,
): TargetTypes {
): [targetType: TargetTypes, isArray: boolean, isCustom: boolean] {
const staticTypeName = staticType.name;
const isCustomType = !ALL_TYPES.includes(staticType);

if (!AVAILABLE_TYPES.includes(staticType)) {
if (!isCustomType && !AVAILABLE_TYPES.includes(staticType)) {
throw new DefinitionError(`Type '${staticTypeName}' is not supported.`, className, propertyKey);
}

if (isCustomType && !isRegistered(staticType)) {
throw new DefinitionError(`Type '${staticTypeName}' is not registered.`, className, propertyKey);
}

if (PRIMITIVE_TYPES.includes(staticType)) {
if (staticType === Number) {
if (!userType || userType === Number) {
Expand Down Expand Up @@ -77,29 +82,25 @@ export function checkType(
);
}

const [arrayOfType] = userType;
if (arrayOfType === Number) {
const [itemType] = userType;
if (itemType === Number) {
throw new DefinitionError(
`You can only use 'Int' or 'Float' for numeric array item.`,
className,
propertyKey,
);
}

if (!isRegistered(arrayOfType)) {
throw new DefinitionError(`Type '${nameOf(arrayOfType)}' is not registered.`, className, propertyKey);
if (!isRegistered(itemType)) {
throw new DefinitionError(`Type '${nameOf(itemType)}' is not registered.`, className, propertyKey);
}

if (!ARRAY_ITEM_TYPES.includes(arrayOfType)) {
throw new DefinitionError(
`Array of type '${nameOf(arrayOfType)}' is not supported.`,
className,
propertyKey,
);
if (!ARRAY_ITEM_TYPES.includes(itemType)) {
throw new DefinitionError(`Array of type '${nameOf(itemType)}' is not supported.`, className, propertyKey);
}

return arrayOfType;
return [itemType, true, !ALL_TYPES.includes(itemType)];
}

return userType || staticType;
return [userType || staticType, false, isCustomType];
}
5 changes: 5 additions & 0 deletions src/utils/generate-table.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ describe("generateTableFromField() Function", () => {
generateTableFromField({
fieldName: "test",
type: String,
isCustom: false,
userData: {
description: "test",
nullable: false,
Expand All @@ -18,6 +19,7 @@ describe("generateTableFromField() Function", () => {
generateTableFromField({
fieldName: "test",
type: Boolean,
isCustom: false,
userData: {
description: "test",
nullable: true,
Expand All @@ -27,6 +29,7 @@ describe("generateTableFromField() Function", () => {
fieldName: "test",
type: Boolean,
isArray: true,
isCustom: false,
userData: {
description: "test",
},
Expand All @@ -35,12 +38,14 @@ describe("generateTableFromField() Function", () => {
fieldName: "test",
type: Int,
isArray: true,
isCustom: false,
userData: {},
}),
generateTableFromField({
fieldName: "test",
type: Float,
isArray: true,
isCustom: false,
userData: {},
}),
]).toMatchSnapshot();
Expand Down
9 changes: 7 additions & 2 deletions src/utils/generate-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ import markdownTable from "markdown-table";

import { FieldData } from "@utils/types";

export function generateTableFromField(field: FieldData) {
export function generateTableFromField(field: FieldData, shouldLink = false) {
const rows: string[][] = [];

let typeName = field.type.name;
if (shouldLink && field.isCustom) {
typeName = `[${typeName}](#type-${typeName.toLowerCase()})`;
}

rows.push(["Name", "Description"]);
rows.push(["Type", `${field.type.name}${field.isArray ? "[]" : ""}`]);
rows.push(["Type", `${typeName}${field.isArray ? "[]" : ""}`]);
rows.push(["Nullable", field.userData.nullable ? "✔️ Yes" : "❌ No"]);

if (field.userData.description) {
Expand Down
36 changes: 36 additions & 0 deletions src/utils/type-storage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe("TypeStorage class", () => {
field: {
fieldName: "test",
type: String,
isCustom: false,
userData: {
description: "test",
nullable: false,
Expand All @@ -43,6 +44,7 @@ describe("TypeStorage class", () => {
field: {
fieldName: "test",
type: String,
isCustom: false,
userData: {
description: "test",
nullable: false,
Expand Down Expand Up @@ -74,4 +76,38 @@ describe("TypeStorage class", () => {
name: "test",
});
});

it("should provide correct default description for field data", () => {
target.collectFieldData({
classType: String,
field: {
fieldName: "test",
type: String,
isArray: false,
isCustom: false,
userData: {},
},
});

target.collectFieldData({
classType: String,
field: {
fieldName: "test2",
type: String,
isArray: true,
isCustom: false,
userData: {},
},
});

expect(target.classes[0].fieldMap["test"].userData).toStrictEqual({
nullable: false,
description: "field 'test' with type 'String'.",
});

expect(target.classes[0].fieldMap["test2"].userData).toStrictEqual({
nullable: false,
description: "field 'test2' with type 'String[]'.",
});
});
});
1 change: 1 addition & 0 deletions src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface BaseFieldData {
export interface FieldData extends BaseFieldData {
type: TargetTypes;
isArray?: boolean;
isCustom: boolean;
}

export interface ClassData {
Expand Down

0 comments on commit ec3fbc8

Please sign in to comment.