Skip to content

Commit 0655c0f

Browse files
jbl428imdudu1
andcommitted
feat: implement url builder
Co-authored-by: imdudu1 <[email protected]>
1 parent 13795b0 commit 0655c0f

File tree

2 files changed

+161
-0
lines changed

2 files changed

+161
-0
lines changed

lib/utils/url-builder.spec.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { describe, test, expect } from "vitest";
2+
import { URLBuilder } from "./url-builder";
3+
import {
4+
type PathVariableMetadata,
5+
type RequestParamMetadata,
6+
} from "../decorators";
7+
8+
describe("URLBuilder", () => {
9+
test("should build with base url", () => {
10+
// given
11+
const host = "https://example.com";
12+
const path = "//api/1";
13+
const args = [1];
14+
const urlBuilder = new URLBuilder(host, path, args);
15+
16+
// when
17+
const actual = urlBuilder.build();
18+
19+
// then
20+
expect(actual).toBe("https://example.com/api/1");
21+
});
22+
23+
test("should replace url with given path variable metadata", () => {
24+
// given
25+
const host = "https://example.com";
26+
const path = "api/users/{id}";
27+
const args = [1, 2];
28+
const pathParam: PathVariableMetadata = new Map([[1, "id"]]);
29+
const urlBuilder = new URLBuilder(host, path, args, { pathParam });
30+
31+
// when
32+
const actual = urlBuilder.build();
33+
34+
// then
35+
expect(actual).toBe("https://example.com/api/users/2");
36+
});
37+
38+
test("should append query string", () => {
39+
// given
40+
const host = "https://example.com";
41+
const path = "";
42+
const args = ["search"];
43+
const queryParam: RequestParamMetadata = new Map([[0, "keyword"]]);
44+
const urlBuilder = new URLBuilder(host, path, args, { queryParam });
45+
46+
// when
47+
const actual = urlBuilder.build();
48+
49+
// then
50+
expect(actual).toBe("https://example.com?keyword=search");
51+
});
52+
53+
test("should append query string when provided as json", () => {
54+
// given
55+
const host = "https://example.com";
56+
const path = "api/user";
57+
const args = [{ keyword: "search" }];
58+
const queryParam: RequestParamMetadata = new Map([[0, undefined]]);
59+
const urlBuilder = new URLBuilder(host, path, args, { queryParam });
60+
61+
// when
62+
const actual = urlBuilder.build();
63+
64+
// then
65+
expect(actual).toBe("https://example.com/api/user?keyword=search");
66+
});
67+
});

lib/utils/url-builder.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import {
2+
type PathVariableMetadata,
3+
type RequestParamMetadata,
4+
} from "../decorators";
5+
6+
export class URLBuilder {
7+
#pathParams: Array<[number, string]> = [];
8+
#queryParams: Array<[number, string | undefined]> = [];
9+
10+
constructor(
11+
private readonly host: string,
12+
private readonly path: string,
13+
private readonly args: any[],
14+
metadata: {
15+
pathParam?: PathVariableMetadata;
16+
queryParam?: RequestParamMetadata;
17+
} = {}
18+
) {
19+
this.#pathParams = this.toArray(metadata.pathParam);
20+
this.#queryParams = this.toArray(metadata.queryParam);
21+
}
22+
23+
build(): string {
24+
return this.replacePathVariable() + this.appendQueryParams();
25+
}
26+
27+
private replacePathVariable(): string {
28+
return this.#pathParams.reduce(
29+
(url, [index, value]) =>
30+
url.replace(new RegExp(`{${value}}`, "g"), this.args[index]),
31+
this.url
32+
);
33+
}
34+
35+
private appendQueryParams(): string {
36+
if (this.#queryParams.length === 0) {
37+
return "";
38+
}
39+
40+
const searchParams = new URLSearchParams();
41+
this.#queryParams.forEach(([paramIndex, queryParamKey]) => {
42+
if (typeof queryParamKey !== "undefined") {
43+
searchParams.set(queryParamKey, this.args[paramIndex]);
44+
return;
45+
}
46+
47+
this.toArray<string, unknown>(this.args[paramIndex]).forEach(
48+
([key, value]) => {
49+
searchParams.set(key, `${value?.toString() ?? ""}`);
50+
}
51+
);
52+
});
53+
54+
return "?" + searchParams.toString();
55+
}
56+
57+
get url(): string {
58+
if (this.path === "") {
59+
return this.host;
60+
}
61+
62+
if (this.isStartProtocol()) {
63+
const [protocol, host] = this.host.split("://");
64+
65+
return protocol + "://" + this.replaceSlash(`${host}/${this.path}`);
66+
}
67+
68+
return this.replaceSlash(`${this.host}/${this.path}`);
69+
}
70+
71+
private isStartProtocol(): boolean {
72+
return this.host.match(/^https?:\/\//) != null;
73+
}
74+
75+
private replaceSlash(url: string): string {
76+
return url.replace(/\/{2,}/g, "/");
77+
}
78+
79+
private toArray<A, B>(value: unknown): Array<[A, B]> {
80+
if (typeof value === "undefined") {
81+
return [];
82+
}
83+
84+
if (value instanceof Map) {
85+
return [...value.entries()];
86+
}
87+
88+
if (typeof value === "object" && value !== null) {
89+
return Object.entries(value) as Array<[A, B]>;
90+
}
91+
92+
return [];
93+
}
94+
}

0 commit comments

Comments
 (0)