diff --git a/.changeset/thin-coats-bake.md b/.changeset/thin-coats-bake.md new file mode 100644 index 00000000000..4e24c68f6b7 --- /dev/null +++ b/.changeset/thin-coats-bake.md @@ -0,0 +1,8 @@ +--- +"@effect/platform": patch +--- + +`Url` module has been introduced: + +- immutable setters with dual-function api +- integration with `UrlParams` diff --git a/packages/platform/src/Error.ts b/packages/platform/src/Error.ts index d1354823527..d042b7cf08e 100644 --- a/packages/platform/src/Error.ts +++ b/packages/platform/src/Error.ts @@ -65,7 +65,7 @@ export declare namespace PlatformError { export interface Base { readonly [PlatformErrorTypeId]: typeof PlatformErrorTypeId readonly _tag: string - readonly module: "Clipboard" | "Command" | "FileSystem" | "KeyValueStore" | "Path" | "Stream" | "Terminal" + readonly module: "Clipboard" | "Command" | "FileSystem" | "KeyValueStore" | "Path" | "Stream" | "Terminal" | "Url" readonly method: string readonly message: string } diff --git a/packages/platform/src/Url.ts b/packages/platform/src/Url.ts new file mode 100644 index 00000000000..6a43f606c34 --- /dev/null +++ b/packages/platform/src/Url.ts @@ -0,0 +1,150 @@ +/** + * @since 1.0.0 + */ +import * as Either from "effect/Either" +import { dual } from "effect/Function" +import * as Error from "./Error.js" +import * as UrlParams from "./UrlParams.js" + +/** @internal */ +const immutableSetter = <M>(clone: (mutable: M) => M) => +<P extends keyof M>(property: P): { + (value: M[P]): (mutable: M) => M + (mutable: M, value: M[P]): M +} => + dual(2, (mutable: M, value: M[P]) => { + const result = clone(mutable) + result[property] = value + return result + }) +/** @internal */ +const immutableURLSetter = immutableSetter<URL>((url) => new URL(url)) +/** + * @since 1.0.0 + * @category constructors + */ +export const make: { + (url: string | URL, base?: string | URL | undefined): Either.Either<URL, Error.BadArgument> +} = (url, base) => + Either.try({ + try: () => new URL(url, base), + catch: (cause) => + Error.BadArgument({ + module: "Url", + method: "make", + message: cause instanceof globalThis.Error ? cause.message : "Invalid input" + }) + }) +/** + * @since 1.0.0 + * @category utils + */ +export const setHash: { + (hash: string): (url: URL) => URL + (url: URL, hash: string): URL +} = immutableURLSetter("hash") +/** + * @since 1.0.0 + * @category utils + */ +export const setHost: { + (host: string): (url: URL) => URL + (url: URL, host: string): URL +} = immutableURLSetter("host") +/** + * @since 1.0.0 + * @category utils + */ +export const setHostname: { + (hostname: string): (url: URL) => URL + (url: URL, hostname: string): URL +} = immutableURLSetter("hostname") +/** + * @since 1.0.0 + * @category utils + */ +export const setHref: { + (href: string): (url: URL) => URL + (url: URL, href: string): URL +} = immutableURLSetter("href") +/** + * @since 1.0.0 + * @category utils + */ +export const setPassword: { + (password: string): (url: URL) => URL + (url: URL, password: string): URL +} = immutableURLSetter("password") +/** + * @since 1.0.0 + * @category utils + */ +export const setPathname: { + (pathname: string): (url: URL) => URL + (url: URL, pathname: string): URL +} = immutableURLSetter("pathname") +/** + * @since 1.0.0 + * @category utils + */ +export const setPort: { + (port: string): (url: URL) => URL + (url: URL, port: string): URL +} = immutableURLSetter("port") +/** + * @since 1.0.0 + * @category utils + */ +export const setProtocol: { + (protocol: string): (url: URL) => URL + (url: URL, protocol: string): URL +} = immutableURLSetter("protocol") +/** + * @since 1.0.0 + * @category utils + */ +export const setSearch: { + (search: string): (url: URL) => URL + (url: URL, search: string): URL +} = immutableURLSetter("search") +/** + * @since 1.0.0 + * @category utils + */ +export const setUsername: { + (username: string): (url: URL) => URL + (url: URL, username: string): URL +} = immutableURLSetter("username") +/** + * @since 1.0.0 + * @category utils + */ +export const setUrlParams: { + (searchParams: UrlParams.UrlParams): (url: URL) => URL + (url: URL, username: UrlParams.UrlParams): URL +} = dual(2, (url: URL, searchParams: UrlParams.UrlParams) => { + const result = new URL(url) + result.search = UrlParams.toString(searchParams) + return result +}) +/** + * @since 1.0.0 + * @category getters + */ +export const urlParams: { + (url: URL): UrlParams.UrlParams +} = (url) => 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) => { + const urlParams = UrlParams.fromInput(url.searchParams) + const newUrlParams = f(urlParams) + const result = new URL(url) + result.search = UrlParams.toString(newUrlParams) + return result +}) diff --git a/packages/platform/src/index.ts b/packages/platform/src/index.ts index bf627d75004..e0a8523001e 100644 --- a/packages/platform/src/index.ts +++ b/packages/platform/src/index.ts @@ -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 */ diff --git a/packages/platform/test/Url.test.ts b/packages/platform/test/Url.test.ts new file mode 100644 index 00000000000..c9abb7426f2 --- /dev/null +++ b/packages/platform/test/Url.test.ts @@ -0,0 +1,28 @@ +import * as Url from "@effect/platform/Url" +import * as UrlParams from "@effect/platform/UrlParams" +import { assert, describe, it } from "@effect/vitest" +import { Effect } from "effect" + +describe("Url", () => { + const testURL = new URL("https://example.com/test") + + describe("make", () => { + it.effect("immutable", () => + Effect.gen(function*() { + const url = yield* Url.make(testURL) + assert.notStrictEqual(url, testURL) + })) + }) + 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, (x) => UrlParams.append(x, "key", "value")) + assert.notStrictEqual(paramsUrl, testURL) + assert.strictEqual(paramsUrl.toString(), "https://example.com/test?key=value") + }) +})