Skip to content

Commit 8cd7319

Browse files
Url module (#4177)
Co-authored-by: Tim <hello@timsmart.co>
1 parent c1a0339 commit 8cd7319

4 files changed

Lines changed: 215 additions & 0 deletions

File tree

.changeset/thin-coats-bake.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@effect/platform": patch
3+
---
4+
5+
`Url` module has been introduced:
6+
7+
- immutable setters with dual-function api
8+
- integration with `UrlParams`

packages/platform/src/Url.ts

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/**
2+
* @since 1.0.0
3+
*/
4+
import * as Cause from "effect/Cause"
5+
import * as Either from "effect/Either"
6+
import { dual } from "effect/Function"
7+
import * as UrlParams from "./UrlParams.js"
8+
9+
/**
10+
* @since 1.0.0
11+
* @category constructors
12+
*/
13+
export const fromString: {
14+
(url: string, base?: string | URL | undefined): Either.Either<URL, Cause.IllegalArgumentException>
15+
} = (url, base) =>
16+
Either.try({
17+
try: () => new URL(url, base),
18+
catch: (cause) =>
19+
new Cause.IllegalArgumentException(cause instanceof globalThis.Error ? cause.message : "Invalid input")
20+
})
21+
22+
/**
23+
* @since 1.0.0
24+
* @category utils
25+
*/
26+
export const mutate: {
27+
(f: (url: URL) => void): (self: URL) => URL
28+
(self: URL, f: (url: URL) => void): URL
29+
} = dual(2, (self: URL, f: (url: URL) => void) => {
30+
const copy = new URL(self)
31+
f(copy)
32+
return copy
33+
})
34+
35+
/** @internal */
36+
const immutableURLSetter = <P extends keyof URL>(property: P): {
37+
(value: URL[P]): (url: URL) => URL
38+
(url: URL, value: URL[P]): URL
39+
} =>
40+
dual(2, (url: URL, value: URL[P]) =>
41+
mutate(url, (url) => {
42+
url[property] = value
43+
}))
44+
45+
/**
46+
* @since 1.0.0
47+
* @category setters
48+
*/
49+
export const setHash: {
50+
(hash: string): (url: URL) => URL
51+
(url: URL, hash: string): URL
52+
} = immutableURLSetter("hash")
53+
54+
/**
55+
* @since 1.0.0
56+
* @category setters
57+
*/
58+
export const setHost: {
59+
(host: string): (url: URL) => URL
60+
(url: URL, host: string): URL
61+
} = immutableURLSetter("host")
62+
63+
/**
64+
* @since 1.0.0
65+
* @category setters
66+
*/
67+
export const setHostname: {
68+
(hostname: string): (url: URL) => URL
69+
(url: URL, hostname: string): URL
70+
} = immutableURLSetter("hostname")
71+
/**
72+
* @since 1.0.0
73+
* @category setters
74+
*/
75+
export const setHref: {
76+
(href: string): (url: URL) => URL
77+
(url: URL, href: string): URL
78+
} = immutableURLSetter("href")
79+
80+
/**
81+
* @since 1.0.0
82+
* @category setters
83+
*/
84+
export const setPassword: {
85+
(password: string): (url: URL) => URL
86+
(url: URL, password: string): URL
87+
} = immutableURLSetter("password")
88+
89+
/**
90+
* @since 1.0.0
91+
* @category setters
92+
*/
93+
export const setPathname: {
94+
(pathname: string): (url: URL) => URL
95+
(url: URL, pathname: string): URL
96+
} = immutableURLSetter("pathname")
97+
98+
/**
99+
* @since 1.0.0
100+
* @category setters
101+
*/
102+
export const setPort: {
103+
(port: string): (url: URL) => URL
104+
(url: URL, port: string): URL
105+
} = immutableURLSetter("port")
106+
107+
/**
108+
* @since 1.0.0
109+
* @category setters
110+
*/
111+
export const setProtocol: {
112+
(protocol: string): (url: URL) => URL
113+
(url: URL, protocol: string): URL
114+
} = immutableURLSetter("protocol")
115+
116+
/**
117+
* @since 1.0.0
118+
* @category setters
119+
*/
120+
export const setSearch: {
121+
(search: string): (url: URL) => URL
122+
(url: URL, search: string): URL
123+
} = immutableURLSetter("search")
124+
125+
/**
126+
* @since 1.0.0
127+
* @category setters
128+
*/
129+
export const setUsername: {
130+
(username: string): (url: URL) => URL
131+
(url: URL, username: string): URL
132+
} = immutableURLSetter("username")
133+
134+
/**
135+
* @since 1.0.0
136+
* @category setters
137+
*/
138+
export const setUrlParams: {
139+
(urlParams: UrlParams.UrlParams): (url: URL) => URL
140+
(url: URL, urlParams: UrlParams.UrlParams): URL
141+
} = dual(2, (url: URL, searchParams: UrlParams.UrlParams) =>
142+
mutate(url, (url) => {
143+
url.search = UrlParams.toString(searchParams)
144+
}))
145+
146+
/**
147+
* @since 1.0.0
148+
* @category getters
149+
*/
150+
export const urlParams = (url: URL): UrlParams.UrlParams => UrlParams.fromInput(url.searchParams)
151+
152+
/**
153+
* @since 1.0.0
154+
* @category utils
155+
*/
156+
export const modifyUrlParams: {
157+
(f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams): (url: URL) => URL
158+
(url: URL, f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams): URL
159+
} = dual(2, (url: URL, f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams) =>
160+
mutate(url, (url) => {
161+
const params = f(UrlParams.fromInput(url.searchParams))
162+
url.search = UrlParams.toString(params)
163+
}))

packages/platform/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,11 @@ export * as Terminal from "./Terminal.js"
249249
*/
250250
export * as Transferable from "./Transferable.js"
251251

252+
/**
253+
* @since 1.0.0
254+
*/
255+
export * as Url from "./Url.js"
256+
252257
/**
253258
* @since 1.0.0
254259
*/

packages/platform/test/Url.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import * as Url from "@effect/platform/Url"
2+
import * as UrlParams from "@effect/platform/UrlParams"
3+
import { assert, describe, it } from "@effect/vitest"
4+
import { Cause, Effect } from "effect"
5+
import { constVoid } from "effect/Function"
6+
7+
describe("Url", () => {
8+
const testURL = new URL("https://example.com/test")
9+
10+
describe("mutate", () => {
11+
it.effect("immutable", () =>
12+
Effect.gen(function*() {
13+
const url = Url.mutate(testURL, constVoid)
14+
assert.notStrictEqual(url, testURL)
15+
}))
16+
})
17+
18+
describe("fromString", () => {
19+
it.effect("fails on incorrect url", () =>
20+
Effect.gen(function*() {
21+
const error = yield* Url.fromString("??").pipe(Effect.flip)
22+
assert.instanceOf(error, Cause.IllegalArgumentException)
23+
}))
24+
})
25+
26+
describe("setters", () => {
27+
it("immutable", () => {
28+
const hashUrl = Url.setHash(testURL, "test")
29+
assert.notStrictEqual(hashUrl, testURL)
30+
assert.strictEqual(hashUrl.toString(), "https://example.com/test#test")
31+
})
32+
})
33+
34+
it("modifyUrlParams", () => {
35+
const paramsUrl = Url.modifyUrlParams(testURL, UrlParams.append("key", "value"))
36+
assert.notStrictEqual(paramsUrl, testURL)
37+
assert.strictEqual(paramsUrl.toString(), "https://example.com/test?key=value")
38+
})
39+
})

0 commit comments

Comments
 (0)