Skip to content

Commit e1472b7

Browse files
port Url from v3 (#1732)
Co-authored-by: Tim <hello@timsmart.co>
1 parent 47d97ea commit e1472b7

File tree

4 files changed

+432
-0
lines changed

4 files changed

+432
-0
lines changed

.changeset/three-corners-sort.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"effect": patch
3+
---
4+
5+
port Url module from v3
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
/**
2+
* @since 4.0.0
3+
*/
4+
import * as Cause from "../../Cause.ts"
5+
import { dual } from "../../Function.ts"
6+
import * as Redacted from "../../Redacted.ts"
7+
import * as Result from "../../Result.ts"
8+
import * as UrlParams from "./UrlParams.ts"
9+
10+
/**
11+
* Parses a URL string into a `URL` object, returning an `Result` type for safe
12+
* error handling.
13+
*
14+
* **Details**
15+
*
16+
* This function converts a string into a `URL` object, enabling safe URL
17+
* parsing with built-in error handling. If the string is invalid or fails to
18+
* parse, this function does not throw an error; instead, it wraps the error in
19+
* a `IllegalArgumentError` and returns it as the `Failure` value of an
20+
* `Result`. The `Success` value contains the successfully parsed `URL`.
21+
*
22+
* An optional `base` parameter can be provided to resolve relative URLs. If
23+
* specified, the function interprets the input `url` as relative to this
24+
* `base`. This is especially useful when dealing with URLs that might not be
25+
* fully qualified.
26+
*
27+
* **Example**
28+
*
29+
* ```ts
30+
* import { Url } from "effect/unstable/http"
31+
* import { Result } from "effect"
32+
*
33+
* // Parse an absolute URL
34+
* //
35+
* // ┌─── Result<URL, IllegalArgumentError>
36+
* // ▼
37+
* const parsed = Url.fromString("https://example.com/path")
38+
*
39+
* if (Result.isSuccess(parsed)) {
40+
* console.log("Parsed URL:", parsed.success.toString())
41+
* } else {
42+
* console.log("Error:", parsed.failure.message)
43+
* }
44+
* // Output: Parsed URL: https://example.com/path
45+
*
46+
* // Parse a relative URL with a base
47+
* const relativeParsed = Url.fromString("/relative-path", "https://example.com")
48+
*
49+
* if (Result.isSuccess(relativeParsed)) {
50+
* console.log("Parsed relative URL:", relativeParsed.success.toString())
51+
* } else {
52+
* console.log("Error:", relativeParsed.failure.message)
53+
* }
54+
* // Output: Parsed relative URL: https://example.com/relative-path
55+
* ```
56+
*
57+
* @since 4.0.0
58+
* @category Constructors
59+
*/
60+
export const fromString: {
61+
(url: string, base?: string | URL | undefined): Result.Result<URL, Cause.IllegalArgumentError>
62+
} = (url, base) =>
63+
Result.try({
64+
try: () => new URL(url, base),
65+
catch: () =>
66+
new Cause.IllegalArgumentError(`Invalid URL: "${url}"${base !== undefined ? ` with base "${base}"` : ""}`)
67+
})
68+
69+
/**
70+
* This function clones the original `URL` object and applies a callback to the
71+
* clone, allowing multiple updates at once.
72+
*
73+
* **Example**
74+
*
75+
* ```ts
76+
* import { Url } from "effect/unstable/http"
77+
*
78+
* const myUrl = new URL("https://example.com")
79+
*
80+
* const mutatedUrl = Url.mutate(myUrl, (url) => {
81+
* url.username = "user"
82+
* url.password = "pass"
83+
* })
84+
*
85+
* console.log("Mutated:", mutatedUrl.toString())
86+
* // Output: Mutated: https://user:pass@example.com/
87+
* ```
88+
*
89+
* @since 4.0.0
90+
* @category Modifiers
91+
*/
92+
export const mutate: {
93+
(f: (url: URL) => void): (self: URL) => URL
94+
(self: URL, f: (url: URL) => void): URL
95+
} = dual(2, (self: URL, f: (url: URL) => void) => {
96+
const copy = new URL(self)
97+
f(copy)
98+
return copy
99+
})
100+
101+
/** @internal */
102+
const immutableURLSetter = <P extends keyof URL, A = never>(property: P): {
103+
(value: URL[P] | A): (url: URL) => URL
104+
(url: URL, value: URL[P] | A): URL
105+
} =>
106+
dual(2, (url: URL, value: URL[P]) =>
107+
mutate(url, (url) => {
108+
url[property] = value
109+
}))
110+
111+
/**
112+
* Updates the hash fragment of the URL.
113+
*
114+
* @since 4.0.0
115+
* @category Setters
116+
*/
117+
export const setHash: {
118+
(hash: string): (url: URL) => URL
119+
(url: URL, hash: string): URL
120+
} = immutableURLSetter("hash")
121+
122+
/**
123+
* Updates the host (domain and port) of the URL.
124+
*
125+
* @since 4.0.0
126+
* @category Setters
127+
*/
128+
export const setHost: {
129+
(host: string): (url: URL) => URL
130+
(url: URL, host: string): URL
131+
} = immutableURLSetter("host")
132+
133+
/**
134+
* Updates the domain of the URL without modifying the port.
135+
*
136+
* @since 4.0.0
137+
* @category Setters
138+
*/
139+
export const setHostname: {
140+
(hostname: string): (url: URL) => URL
141+
(url: URL, hostname: string): URL
142+
} = immutableURLSetter("hostname")
143+
144+
/**
145+
* Replaces the entire URL string.
146+
*
147+
* @since 4.0.0
148+
* @category Setters
149+
*/
150+
export const setHref: {
151+
(href: string): (url: URL) => URL
152+
(url: URL, href: string): URL
153+
} = immutableURLSetter("href")
154+
155+
/**
156+
* Updates the password used for authentication.
157+
*
158+
* @since 4.0.0
159+
* @category Setters
160+
*/
161+
export const setPassword: {
162+
(password: string | Redacted.Redacted): (url: URL) => URL
163+
(url: URL, password: string | Redacted.Redacted): URL
164+
} = dual(2, (url: URL, password: string | Redacted.Redacted) =>
165+
mutate(url, (url) => {
166+
url.password = typeof password === "string"
167+
? password :
168+
Redacted.value(password)
169+
}))
170+
171+
/**
172+
* Updates the path of the URL.
173+
*
174+
* @since 4.0.0
175+
* @category Setters
176+
*/
177+
export const setPathname: {
178+
(pathname: string): (url: URL) => URL
179+
(url: URL, pathname: string): URL
180+
} = immutableURLSetter("pathname")
181+
182+
/**
183+
* Updates the port of the URL.
184+
*
185+
* @since 4.0.0
186+
* @category Setters
187+
*/
188+
export const setPort: {
189+
(port: string | number): (url: URL) => URL
190+
(url: URL, port: string | number): URL
191+
} = immutableURLSetter("port")
192+
193+
/**
194+
* Updates the protocol (e.g., `http`, `https`).
195+
*
196+
* @since 4.0.0
197+
* @category Setters
198+
*/
199+
export const setProtocol: {
200+
(protocol: string): (url: URL) => URL
201+
(url: URL, protocol: string): URL
202+
} = immutableURLSetter("protocol")
203+
204+
/**
205+
* Updates the query string of the URL.
206+
*
207+
* @since 4.0.0
208+
* @category Setters
209+
*/
210+
export const setSearch: {
211+
(search: string): (url: URL) => URL
212+
(url: URL, search: string): URL
213+
} = immutableURLSetter("search")
214+
215+
/**
216+
* Updates the username used for authentication.
217+
*
218+
* @since 4.0.0
219+
* @category Setters
220+
*/
221+
export const setUsername: {
222+
(username: string): (url: URL) => URL
223+
(url: URL, username: string): URL
224+
} = immutableURLSetter("username")
225+
226+
/**
227+
* Updates the query parameters of a URL.
228+
*
229+
* **Details**
230+
*
231+
* This function allows you to set or replace the query parameters of a `URL`
232+
* object using the provided `UrlParams`. It creates a new `URL` object with the
233+
* updated parameters, leaving the original object unchanged.
234+
*
235+
* **Example**
236+
*
237+
* ```ts
238+
* import { Url, UrlParams } from "effect/unstable/http"
239+
*
240+
* const myUrl = new URL("https://example.com?foo=bar")
241+
*
242+
* // Write parameters
243+
* const updatedUrl = Url.setUrlParams(
244+
* myUrl,
245+
* UrlParams.fromInput([["key", "value"]])
246+
* )
247+
*
248+
* console.log(updatedUrl.toString())
249+
* // Output: https://example.com/?key=value
250+
* ```
251+
*
252+
* @since 4.0.0
253+
* @category Setters
254+
*/
255+
export const setUrlParams: {
256+
(urlParams: UrlParams.UrlParams): (url: URL) => URL
257+
(url: URL, urlParams: UrlParams.UrlParams): URL
258+
} = dual(2, (url: URL, searchParams: UrlParams.UrlParams) =>
259+
mutate(url, (url) => {
260+
url.search = UrlParams.toString(searchParams)
261+
}))
262+
263+
/**
264+
* Retrieves the query parameters from a URL.
265+
*
266+
* **Details**
267+
*
268+
* This function extracts the query parameters from a `URL` object and returns
269+
* them as `UrlParams`. The resulting structure can be easily manipulated or
270+
* inspected.
271+
*
272+
* **Example**
273+
*
274+
* ```ts
275+
* import { Url } from "effect/unstable/http"
276+
*
277+
* const myUrl = new URL("https://example.com?foo=bar")
278+
*
279+
* // Read parameters
280+
* const params = Url.urlParams(myUrl)
281+
*
282+
* console.log(params)
283+
* // Output: [ [ 'foo', 'bar' ] ]
284+
* ```
285+
*
286+
* @since 4.0.0
287+
* @category Getters
288+
*/
289+
export const urlParams = (url: URL): UrlParams.UrlParams => UrlParams.fromInput(url.searchParams)
290+
291+
/**
292+
* Reads, modifies, and updates the query parameters of a URL.
293+
*
294+
* **Details**
295+
*
296+
* This function provides a functional way to interact with query parameters by
297+
* reading the current parameters, applying a transformation function, and then
298+
* writing the updated parameters back to the URL. It returns a new `URL` object
299+
* with the modified parameters, ensuring immutability.
300+
*
301+
* **Example**
302+
*
303+
* ```ts
304+
* import { Url, UrlParams } from "effect/unstable/http"
305+
*
306+
* const myUrl = new URL("https://example.com?foo=bar")
307+
*
308+
* const changedUrl = Url.modifyUrlParams(myUrl, UrlParams.append("key", "value"))
309+
*
310+
* console.log(changedUrl.toString())
311+
* // Output: https://example.com/?foo=bar&key=value
312+
* ```
313+
*
314+
* @since 4.0.0
315+
* @category Modifiers
316+
*/
317+
export const modifyUrlParams: {
318+
(f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams): (url: URL) => URL
319+
(url: URL, f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams): URL
320+
} = dual(2, (url: URL, f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams) =>
321+
mutate(url, (url) => {
322+
const params = f(UrlParams.fromInput(url.searchParams))
323+
url.search = UrlParams.toString(params)
324+
}))

packages/effect/src/unstable/http/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,8 @@ export * as Template from "./Template.ts"
138138
* @since 4.0.0
139139
*/
140140
export * as UrlParams from "./UrlParams.ts"
141+
142+
/**
143+
* @since 4.0.0
144+
*/
145+
export * as Url from "./Url.ts"

0 commit comments

Comments
 (0)