Skip to content

Commit 2a49297

Browse files
Clean template implementation to make it more closed to change (#7)
* refactor: clean template implementation to make it more extendable and closed to change * refactor: use defined constants in the tests
1 parent c87e791 commit 2a49297

File tree

10 files changed

+303
-184
lines changed

10 files changed

+303
-184
lines changed

__tests__/integration/layers.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import fsPromises from "node:fs/promises";
22
import { tmpdir } from "node:os";
33
import { join } from "node:path";
4-
import { defaultLayers } from "@/constants";
4+
import { basePath as defaultBasePath, defaultLayers } from "@/constants";
55
import { createLayersIfNotExists } from "@/layers";
66

77
describe("Layers folders structure", () => {
88
const defaultMainDirectory = "src";
99

10-
let basePath: string;
10+
let basePath: string = defaultBasePath;
1111

1212
async function getFolders(...paths: string[]) {
1313
return await fsPromises.readdir(join(...paths));

__tests__/unit/templates.test.ts

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import templates from "@/templates";
2+
import { Kebab, Pascal } from "@/utils/formats";
23
import { template } from "@/utils/tags";
34
import {
45
factoryTemplateMock,
@@ -15,9 +16,9 @@ describe("Code generation", () => {
1516

1617
it("should generate a repository template", () => {
1718
const expected: Component = {
18-
filename: template`${{ value: resource, casing: "kebab" }}.repository.ts`,
19+
filename: template`${new Kebab(resource)}.repository.ts`(),
1920
body: repositoryTemplateMock,
20-
name: template`${{ value: resource, casing: "pascal" }}Repository`,
21+
name: template`${new Pascal(resource)}Repository`(),
2122
};
2223

2324
const result = templates.get("repository")?.(resource);
@@ -28,53 +29,41 @@ describe("Code generation", () => {
2829

2930
it("should generate a service template", () => {
3031
const expected: Component = {
31-
filename: template`${{ value: resource, casing: "kebab" }}.service.ts`,
32+
filename: template`${new Kebab(resource)}.service.ts`(),
3233
body: serviceTemplateMock,
33-
name: template`${{ value: resource, casing: "pascal" }}Service`,
34+
name: template`${new Pascal(resource)}Service`(),
3435
};
3536

3637
const result = templates.get("service")?.(resource);
3738

3839
expect(result).toEqual(expected);
3940
expect(result?.body).toContain(
40-
template`import ${{
41-
value: resource,
42-
casing: "pascal",
43-
}}Repository from "../repository/${{
44-
value: resource,
45-
casing: "kebab",
46-
}}.repository";`,
41+
template`import ${new Pascal(
42+
resource,
43+
)}Repository from "../repository/${new Kebab(resource)}.repository";`(),
4744
);
4845
expect(result?.body).toContain(`export default class ${result?.name}`);
4946
});
5047

5148
it("should generate a factory template", () => {
5249
const expected: Component = {
53-
filename: template`${{ value: resource, casing: "kebab" }}.factory.ts`,
50+
filename: template`${new Kebab(resource)}.factory.ts`(),
5451
body: factoryTemplateMock,
55-
name: template`${{ value: resource, casing: "pascal" }}Factory`,
52+
name: template`${new Pascal(resource)}Factory`(),
5653
};
5754

5855
const result = templates.get("factory")?.(resource);
5956

6057
expect(result).toEqual(expected);
6158
expect(result?.body).toContain(
62-
template`import ${{
63-
value: resource,
64-
casing: "pascal",
65-
}}Repository from "../repository/${{
66-
value: resource,
67-
casing: "kebab",
68-
}}.repository";`,
59+
template`import ${new Pascal(
60+
resource,
61+
)}Repository from "../repository/${new Kebab(resource)}.repository";`(),
6962
);
7063
expect(result?.body).toContain(
71-
template`import ${{
72-
value: resource,
73-
casing: "pascal",
74-
}}Service from "../service/${{
75-
value: resource,
76-
casing: "kebab",
77-
}}.service";`,
64+
template`import ${new Pascal(
65+
resource,
66+
)}Service from "../service/${new Kebab(resource)}.service";`(),
7867
);
7968
expect(result?.body).toContain(`export default class ${result?.name}`);
8069
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { Camel, Kebab, Lower, Pascal, Snake, Upper } from "@/utils/formats";
2+
3+
describe("formats", () => {
4+
const cases = [
5+
{
6+
Class: Upper,
7+
input: "hello world",
8+
expected: "HELLO WORLD",
9+
},
10+
{
11+
Class: Lower,
12+
input: "HELLO WORLD",
13+
expected: "hello world",
14+
},
15+
{
16+
Class: Camel,
17+
input: "hello world",
18+
expected: "helloWorld",
19+
},
20+
{
21+
Class: Pascal,
22+
input: "hello world",
23+
expected: "HelloWorld",
24+
},
25+
{
26+
Class: Kebab,
27+
input: "hello world",
28+
expected: "hello-world",
29+
},
30+
{
31+
Class: Snake,
32+
input: "hello world",
33+
expected: "hello_world",
34+
},
35+
];
36+
37+
it.each(cases)(
38+
"$Class.name should format '$input' correctly",
39+
({ Class, input, expected }) => {
40+
const instance = new Class(input);
41+
expect(instance.toString()).toBe(expected);
42+
},
43+
);
44+
45+
it("should support symbol-toPrimitive for string coercion", () => {
46+
const instance = new Pascal("hello world");
47+
expect(`${instance}`).toBe("HelloWorld");
48+
});
49+
50+
it("should handle multiple delimiters", () => {
51+
const snake = new Snake("this--is__a test");
52+
const kebab = new Kebab("this--is__a test");
53+
const camel = new Camel("this--is__a test");
54+
55+
expect(snake.toString()).toBe("this_is_a_test");
56+
expect(kebab.toString()).toBe("this-is-a-test");
57+
expect(camel.toString()).toBe("thisIsATest");
58+
});
59+
60+
it("should return empty string for undefined or null values", () => {
61+
expect(new Upper(undefined).toString()).toBe("");
62+
expect(new Kebab(null).toString()).toBe("");
63+
});
64+
});

__tests__/unit/utils/tags.test.ts

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,56 @@
1+
import {
2+
Camel,
3+
type Case,
4+
Kebab,
5+
Lower,
6+
Pascal,
7+
Snake,
8+
Upper,
9+
} from "@/utils/formats";
110
import { template } from "@/utils/tags";
211

312
describe("tags", () => {
413
describe("template", () => {
514
it("should parse a placeholder based on the given values", () => {
6-
for (const [casing, expected] of [
7-
["camel", "camelCase"],
8-
["pascal", "PascalCase"],
9-
["kebab", "kebab-case"],
10-
["snake", "snake_case"],
11-
["upper", "UPPER CASE"],
12-
["lower", "lower case"],
13-
[undefined, "undefined case"],
14-
[null, ""],
15-
] as [Template.Casing, string][]) {
16-
const result = template`${{
17-
value: expected ? `${casing} case` : null,
18-
casing,
19-
}}`;
15+
for (const [Format, expected] of [
16+
[Camel, "camelCase"],
17+
[Pascal, "PascalCase"],
18+
[Kebab, "kebab-case"],
19+
[Snake, "snake_case"],
20+
[Upper, "UPPER CASE"],
21+
[Lower, "lower case"],
22+
] as [new (value: string) => Case, string][]) {
23+
const result = template`${new Format(`${Format.name} case`)}`();
2024
expect(result).toStrictEqual(expected);
2125
}
2226
});
2327

28+
it("should ignore falsy values", () => {
29+
const result = template`${new Pascal(
30+
"user",
31+
)}${undefined}${null}Repository`();
32+
expect(result).toStrictEqual("UserRepository");
33+
});
34+
2435
it("should work as the example", () => {
25-
const result = template`class ${{
26-
value: "user",
27-
casing: "pascal",
28-
}}Repository {}`;
36+
const result = template`class ${new Pascal("user")}Repository {}`();
2937
expect(result).toStrictEqual("class UserRepository {}");
3038
});
39+
40+
describe("compose", () => {
41+
it("should compose the result with a function", () => {
42+
const result = template`${new Pascal("user")}Repository`.compose(
43+
(input) => `${input}Test`,
44+
)();
45+
expect(result).toStrictEqual("UserRepositoryTest");
46+
});
47+
48+
it("should return the composed value when calling thenRender", () => {
49+
const result = template`${new Pascal("user")}Repository`
50+
.compose((input) => `${input}Test`)
51+
.thenRender();
52+
expect(result).toStrictEqual("UserRepositoryTest");
53+
});
54+
});
3155
});
3256
});

src/templates/factory.ts

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,26 @@
1+
import { Kebab, Pascal } from "@/utils/formats";
12
import { template } from "@/utils/tags";
23

34
function body(resource: string) {
4-
return template`import ${{
5-
value: resource,
6-
casing: "pascal",
7-
}}Repository from "../repository/${{ value: resource, casing: "kebab" }}.repository";
8-
import ${{ value: resource, casing: "pascal" }}Service from "../service/${{
9-
value: resource,
10-
casing: "kebab",
11-
}}.service";
5+
return template`import ${new Pascal(
6+
resource,
7+
)}Repository from "../repository/${new Kebab(resource)}.repository";
8+
import ${new Pascal(resource)}Service from "../service/${new Kebab(resource)}.service";
129
13-
export default class ${{ value: resource, casing: "pascal" }}Factory {
10+
export default class ${new Pascal(resource)}Factory {
1411
static getInstance() {
15-
return new ${{ value: resource, casing: "pascal" }}Service(new ${{
16-
value: resource,
17-
casing: "pascal",
18-
}}Repository());
12+
return new ${new Pascal(resource)}Service(new ${new Pascal(resource)}Repository());
1913
}
2014
}
2115
`;
2216
}
2317

24-
function filename(
25-
resource: string,
26-
casing: Extract<Template.Casing, "kebab" | "camel"> = "kebab",
27-
) {
28-
return template`${{ value: resource, casing: casing }}.factory.ts`;
18+
function filename(resource: string) {
19+
return template`${new Kebab(resource)}.factory.ts`;
2920
}
3021

3122
function name(resource: string) {
32-
return template`${{ value: resource, casing: "pascal" }}Factory`;
23+
return template`${new Pascal(resource)}Factory`;
3324
}
3425

3526
/**
@@ -40,8 +31,8 @@ function name(resource: string) {
4031
*/
4132
export function component(resource: string): Component {
4233
return {
43-
name: name(resource),
44-
filename: filename(resource),
45-
body: body(resource),
34+
name: name(resource).render(),
35+
filename: filename(resource).render(),
36+
body: body(resource).render(),
4637
};
4738
}

src/templates/repository.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { Kebab, Pascal } from "@/utils/formats";
12
import { template } from "@/utils/tags";
23

34
function body(resource: string) {
4-
return template`export default class ${{ value: resource, casing: "pascal" }}Repository {
5+
return template`export default class ${new Pascal(resource)}Repository {
56
constructor() {}
67
78
async create(data: any) {
@@ -23,12 +24,12 @@ function body(resource: string) {
2324
`;
2425
}
2526

26-
function filename(resource: string, casing: Template.Casing = "kebab") {
27-
return template`${{ value: resource, casing: casing }}.repository.ts`;
27+
function filename(resource: string) {
28+
return template`${new Kebab(resource)}.repository.ts`;
2829
}
2930

3031
function name(resource: string) {
31-
return template`${{ value: resource, casing: "pascal" }}Repository`;
32+
return template`${new Pascal(resource)}Repository`;
3233
}
3334

3435
/**
@@ -39,8 +40,8 @@ function name(resource: string) {
3940
*/
4041
export function component(resource: string): Component {
4142
return {
42-
name: name(resource),
43-
filename: filename(resource),
44-
body: body(resource),
43+
name: name(resource).render(),
44+
filename: filename(resource).render(),
45+
body: body(resource).render(),
4546
};
4647
}

0 commit comments

Comments
 (0)