Skip to content

Commit

Permalink
Url module (#4177)
Browse files Browse the repository at this point in the history
Co-authored-by: Tim <[email protected]>
  • Loading branch information
KhraksMamtsov and tim-smart authored Jan 13, 2025
1 parent c1a0339 commit 8cd7319
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .changeset/thin-coats-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@effect/platform": patch
---

`Url` module has been introduced:

- immutable setters with dual-function api
- integration with `UrlParams`
163 changes: 163 additions & 0 deletions packages/platform/src/Url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/**
* @since 1.0.0
*/
import * as Cause from "effect/Cause"
import * as Either from "effect/Either"
import { dual } from "effect/Function"
import * as UrlParams from "./UrlParams.js"

/**
* @since 1.0.0
* @category constructors
*/
export const fromString: {
(url: string, base?: string | URL | undefined): Either.Either<URL, Cause.IllegalArgumentException>
} = (url, base) =>
Either.try({
try: () => new URL(url, base),
catch: (cause) =>
new Cause.IllegalArgumentException(cause instanceof globalThis.Error ? cause.message : "Invalid input")
})

/**
* @since 1.0.0
* @category utils
*/
export const mutate: {
(f: (url: URL) => void): (self: URL) => URL
(self: URL, f: (url: URL) => void): URL
} = dual(2, (self: URL, f: (url: URL) => void) => {
const copy = new URL(self)
f(copy)
return copy
})

/** @internal */
const immutableURLSetter = <P extends keyof URL>(property: P): {
(value: URL[P]): (url: URL) => URL
(url: URL, value: URL[P]): URL
} =>
dual(2, (url: URL, value: URL[P]) =>
mutate(url, (url) => {
url[property] = value
}))

/**
* @since 1.0.0
* @category setters
*/
export const setHash: {
(hash: string): (url: URL) => URL
(url: URL, hash: string): URL
} = immutableURLSetter("hash")

/**
* @since 1.0.0
* @category setters
*/
export const setHost: {
(host: string): (url: URL) => URL
(url: URL, host: string): URL
} = immutableURLSetter("host")

/**
* @since 1.0.0
* @category setters
*/
export const setHostname: {
(hostname: string): (url: URL) => URL
(url: URL, hostname: string): URL
} = immutableURLSetter("hostname")
/**
* @since 1.0.0
* @category setters
*/
export const setHref: {
(href: string): (url: URL) => URL
(url: URL, href: string): URL
} = immutableURLSetter("href")

/**
* @since 1.0.0
* @category setters
*/
export const setPassword: {
(password: string): (url: URL) => URL
(url: URL, password: string): URL
} = immutableURLSetter("password")

/**
* @since 1.0.0
* @category setters
*/
export const setPathname: {
(pathname: string): (url: URL) => URL
(url: URL, pathname: string): URL
} = immutableURLSetter("pathname")

/**
* @since 1.0.0
* @category setters
*/
export const setPort: {
(port: string): (url: URL) => URL
(url: URL, port: string): URL
} = immutableURLSetter("port")

/**
* @since 1.0.0
* @category setters
*/
export const setProtocol: {
(protocol: string): (url: URL) => URL
(url: URL, protocol: string): URL
} = immutableURLSetter("protocol")

/**
* @since 1.0.0
* @category setters
*/
export const setSearch: {
(search: string): (url: URL) => URL
(url: URL, search: string): URL
} = immutableURLSetter("search")

/**
* @since 1.0.0
* @category setters
*/
export const setUsername: {
(username: string): (url: URL) => URL
(url: URL, username: string): URL
} = immutableURLSetter("username")

/**
* @since 1.0.0
* @category setters
*/
export const setUrlParams: {
(urlParams: UrlParams.UrlParams): (url: URL) => URL
(url: URL, urlParams: UrlParams.UrlParams): URL
} = dual(2, (url: URL, searchParams: UrlParams.UrlParams) =>
mutate(url, (url) => {
url.search = UrlParams.toString(searchParams)
}))

/**
* @since 1.0.0
* @category getters
*/
export const urlParams = (url: URL): UrlParams.UrlParams => UrlParams.fromInput(url.searchParams)

/**
* @since 1.0.0
* @category utils
*/
export const modifyUrlParams: {
(f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams): (url: URL) => URL
(url: URL, f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams): URL
} = dual(2, (url: URL, f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams) =>
mutate(url, (url) => {
const params = f(UrlParams.fromInput(url.searchParams))
url.search = UrlParams.toString(params)
}))
5 changes: 5 additions & 0 deletions packages/platform/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,11 @@ export * as Terminal from "./Terminal.js"
*/
export * as Transferable from "./Transferable.js"

/**
* @since 1.0.0
*/
export * as Url from "./Url.js"

/**
* @since 1.0.0
*/
Expand Down
39 changes: 39 additions & 0 deletions packages/platform/test/Url.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as Url from "@effect/platform/Url"
import * as UrlParams from "@effect/platform/UrlParams"
import { assert, describe, it } from "@effect/vitest"
import { Cause, Effect } from "effect"
import { constVoid } from "effect/Function"

describe("Url", () => {
const testURL = new URL("https://example.com/test")

describe("mutate", () => {
it.effect("immutable", () =>
Effect.gen(function*() {
const url = Url.mutate(testURL, constVoid)
assert.notStrictEqual(url, testURL)
}))
})

describe("fromString", () => {
it.effect("fails on incorrect url", () =>
Effect.gen(function*() {
const error = yield* Url.fromString("??").pipe(Effect.flip)
assert.instanceOf(error, Cause.IllegalArgumentException)
}))
})

describe("setters", () => {
it("immutable", () => {
const hashUrl = Url.setHash(testURL, "test")
assert.notStrictEqual(hashUrl, testURL)
assert.strictEqual(hashUrl.toString(), "https://example.com/test#test")
})
})

it("modifyUrlParams", () => {
const paramsUrl = Url.modifyUrlParams(testURL, UrlParams.append("key", "value"))
assert.notStrictEqual(paramsUrl, testURL)
assert.strictEqual(paramsUrl.toString(), "https://example.com/test?key=value")
})
})

0 comments on commit 8cd7319

Please sign in to comment.