diff --git a/.changeset/four-scissors-tie.md b/.changeset/four-scissors-tie.md new file mode 100644 index 00000000..503dc296 --- /dev/null +++ b/.changeset/four-scissors-tie.md @@ -0,0 +1,6 @@ +--- +"@effect/platform-node": patch +"@effect/platform": patch +--- + +use branded type for Headers diff --git a/.changeset/many-rats-notice.md b/.changeset/many-rats-notice.md new file mode 100644 index 00000000..18b6ddc7 --- /dev/null +++ b/.changeset/many-rats-notice.md @@ -0,0 +1,6 @@ +--- +"@effect/platform-node": patch +"@effect/platform": patch +--- + +change UrlParams to ReadonlyArray diff --git a/docs/platform/Http/Headers.ts.md b/docs/platform/Http/Headers.ts.md index 2a74be41..b38814e3 100644 --- a/docs/platform/Http/Headers.ts.md +++ b/docs/platform/Http/Headers.ts.md @@ -15,15 +15,20 @@ Added in v1.0.0 - [combinators](#combinators) - [get](#get) - [has](#has) + - [merge](#merge) - [remove](#remove) - [set](#set) - [setAll](#setall) - [constructors](#constructors) - [empty](#empty) - [fromInput](#frominput) + - [unsafeFromRecord](#unsafefromrecord) - [models](#models) - - [Headers (interface)](#headers-interface) + - [Headers (type alias)](#headers-type-alias) - [Input (type alias)](#input-type-alias) +- [type ids](#type-ids) + - [HeadersTypeId](#headerstypeid) + - [HeadersTypeId (type alias)](#headerstypeid-type-alias) --- @@ -52,6 +57,19 @@ export declare const has: { (key: string): (self: Headers) => boolean; (self: He Added in v1.0.0 +## merge + +**Signature** + +```ts +export declare const merge: { + (headers: Headers): (self: Headers) => Headers + (self: Headers, headers: Headers): Headers +} +``` + +Added in v1.0.0 + ## remove **Signature** @@ -107,14 +125,24 @@ export declare const fromInput: (input?: Input) => Headers Added in v1.0.0 +## unsafeFromRecord + +**Signature** + +```ts +export declare const unsafeFromRecord: (input: ReadonlyRecord.ReadonlyRecord) => Headers +``` + +Added in v1.0.0 + # models -## Headers (interface) +## Headers (type alias) **Signature** ```ts -export interface Headers extends ReadonlyRecord.ReadonlyRecord {} +export type Headers = Brand.Branded, HeadersTypeId> ``` Added in v1.0.0 @@ -124,7 +152,29 @@ Added in v1.0.0 **Signature** ```ts -export type Input = Headers | Iterable +export type Input = ReadonlyRecord.ReadonlyRecord | Iterable +``` + +Added in v1.0.0 + +# type ids + +## HeadersTypeId + +**Signature** + +```ts +export declare const HeadersTypeId: typeof HeadersTypeId +``` + +Added in v1.0.0 + +## HeadersTypeId (type alias) + +**Signature** + +```ts +export type HeadersTypeId = typeof HeadersTypeId ``` Added in v1.0.0 diff --git a/docs/platform/Http/Platform.ts.md b/docs/platform/Http/Platform.ts.md index ae9dfa88..a2828e99 100644 --- a/docs/platform/Http/Platform.ts.md +++ b/docs/platform/Http/Platform.ts.md @@ -36,7 +36,7 @@ export declare const make: (impl: { path: string, status: number, statusText: string | undefined, - headers: Record, + headers: Headers.Headers, start: number, end: number | undefined, contentLength: number @@ -45,7 +45,7 @@ export declare const make: (impl: { file: Body.Body.FileLike, status: number, statusText: string | undefined, - headers: Record, + headers: Headers.Headers, options?: FileSystem.StreamOptions | undefined ) => ServerResponse.ServerResponse }) => Effect.Effect diff --git a/docs/platform/Http/UrlParams.ts.md b/docs/platform/Http/UrlParams.ts.md index a6c3115a..65bfbcea 100644 --- a/docs/platform/Http/UrlParams.ts.md +++ b/docs/platform/Http/UrlParams.ts.md @@ -149,7 +149,7 @@ Added in v1.0.0 **Signature** ```ts -export type Input = UrlParams | Readonly> | Iterable | URLSearchParams +export type Input = Readonly> | Iterable | URLSearchParams ``` Added in v1.0.0 @@ -159,7 +159,7 @@ Added in v1.0.0 **Signature** ```ts -export interface UrlParams extends Chunk.Chunk<[string, string]> {} +export interface UrlParams extends ReadonlyArray {} ``` Added in v1.0.0 diff --git a/packages/platform-node/src/internal/http/platform.ts b/packages/platform-node/src/internal/http/platform.ts index d2dd615a..f8431886 100644 --- a/packages/platform-node/src/internal/http/platform.ts +++ b/packages/platform-node/src/internal/http/platform.ts @@ -1,3 +1,4 @@ +import * as Headers from "@effect/platform/Http/Headers" import * as Platform from "@effect/platform/Http/Platform" import * as ServerResponse from "@effect/platform/Http/ServerResponse" import { pipe } from "effect/Function" @@ -24,11 +25,13 @@ export const make = Platform.make({ }, fileWebResponse(file, status, statusText, headers, _options) { return ServerResponse.raw(Readable.fromWeb(file.stream() as any), { - headers: { - ...headers, - "content-type": headers["content-type"] ?? Mime.getType(file.name) ?? "application/octet-stream", - "content-length": file.size.toString() - }, + headers: Headers.merge( + headers, + Headers.unsafeFromRecord({ + "content-type": headers["content-type"] ?? Mime.getType(file.name) ?? "application/octet-stream", + "content-length": file.size.toString() + }) + ), status, statusText }) diff --git a/packages/platform/src/Http/Headers.ts b/packages/platform/src/Http/Headers.ts index f350a8fa..a22f6212 100644 --- a/packages/platform/src/Http/Headers.ts +++ b/packages/platform/src/Http/Headers.ts @@ -1,28 +1,41 @@ /** * @since 1.0.0 */ +import type * as Brand from "effect/Brand" import { dual } from "effect/Function" import type * as Option from "effect/Option" import * as ReadonlyArray from "effect/ReadonlyArray" import * as ReadonlyRecord from "effect/ReadonlyRecord" +/** + * @since 1.0.0 + * @category type ids + */ +export const HeadersTypeId = Symbol.for("@effect/platform/Http/Headers") + +/** + * @since 1.0.0 + * @category type ids + */ +export type HeadersTypeId = typeof HeadersTypeId + /** * @since 1.0.0 * @category models */ -export interface Headers extends ReadonlyRecord.ReadonlyRecord {} +export type Headers = Brand.Branded, HeadersTypeId> /** * @since 1.0.0 * @category models */ -export type Input = Headers | Iterable +export type Input = ReadonlyRecord.ReadonlyRecord | Iterable /** * @since 1.0.0 * @category constructors */ -export const empty: Headers = ReadonlyRecord.empty() +export const empty: Headers = Object.create(null) as Headers /** * @since 1.0.0 @@ -35,13 +48,19 @@ export const fromInput: (input?: Input) => Headers = (input) => { return ReadonlyRecord.fromEntries(ReadonlyArray.map( ReadonlyArray.fromIterable(input), ([k, v]) => [k.toLowerCase(), v] as const - )) + )) as Headers } return ReadonlyRecord.fromEntries( Object.entries(input).map(([k, v]) => [k.toLowerCase(), v]) - ) + ) as Headers } +/** + * @since 1.0.0 + * @category constructors + */ +export const unsafeFromRecord = (input: ReadonlyRecord.ReadonlyRecord): Headers => input as Headers + /** * @since 1.0.0 * @category combinators @@ -96,6 +115,21 @@ export const setAll: { ...fromInput(headers) })) +/** + * @since 1.0.0 + * @category combinators + */ +export const merge: { + (headers: Headers): (self: Headers) => Headers + (self: Headers, headers: Headers): Headers +} = dual< + (headers: Headers) => (self: Headers) => Headers, + (self: Headers, headers: Headers) => Headers +>(2, (self, headers) => ({ + ...self, + ...headers +})) + /** * @since 1.0.0 * @category combinators @@ -106,4 +140,4 @@ export const remove: { } = dual< (key: string) => (self: Headers) => Headers, (self: Headers, key: string) => Headers ->(2, (self, key) => ReadonlyRecord.remove(self, key.toLowerCase())) +>(2, (self, key) => ReadonlyRecord.remove(self, key.toLowerCase()) as Headers) diff --git a/packages/platform/src/Http/Platform.ts b/packages/platform/src/Http/Platform.ts index 4adb6da6..69e5b118 100644 --- a/packages/platform/src/Http/Platform.ts +++ b/packages/platform/src/Http/Platform.ts @@ -8,6 +8,7 @@ import type * as FileSystem from "../FileSystem.js" import * as internal from "../internal/http/platform.js" import type * as Body from "./Body.js" import type * as Etag from "./Etag.js" +import type * as Headers from "./Headers.js" import type * as ServerResponse from "./ServerResponse.js" /** @@ -54,7 +55,7 @@ export const make: ( path: string, status: number, statusText: string | undefined, - headers: Record, + headers: Headers.Headers, start: number, end: number | undefined, contentLength: number @@ -63,7 +64,7 @@ export const make: ( file: Body.Body.FileLike, status: number, statusText: string | undefined, - headers: Record, + headers: Headers.Headers, options?: FileSystem.StreamOptions | undefined ) => ServerResponse.ServerResponse } diff --git a/packages/platform/src/Http/UrlParams.ts b/packages/platform/src/Http/UrlParams.ts index 83ca0c95..a3e386a3 100644 --- a/packages/platform/src/Http/UrlParams.ts +++ b/packages/platform/src/Http/UrlParams.ts @@ -1,40 +1,38 @@ /** * @since 1.0.0 */ -import * as Chunk from "effect/Chunk" import * as Effect from "effect/Effect" import { dual } from "effect/Function" +import * as ReadonlyArray from "effect/ReadonlyArray" /** * @since 1.0.0 * @category models */ -export interface UrlParams extends Chunk.Chunk<[string, string]> {} +export interface UrlParams extends ReadonlyArray {} /** * @since 1.0.0 * @category models */ -export type Input = UrlParams | Readonly> | Iterable | URLSearchParams +export type Input = Readonly> | Iterable | URLSearchParams /** * @since 1.0.0 * @category constructors */ export const fromInput = (input: Input): UrlParams => { - if (Chunk.isChunk(input)) { - return input - } else if (Symbol.iterator in input) { - return Chunk.fromIterable(input) as UrlParams + if (Symbol.iterator in input) { + return ReadonlyArray.fromIterable(input) } - return Chunk.fromIterable(Object.entries(input)) + return ReadonlyArray.fromIterable(Object.entries(input)) } /** * @since 1.0.0 * @category constructors */ -export const empty: UrlParams = Chunk.empty() +export const empty: UrlParams = [] /** * @since 1.0.0 @@ -47,8 +45,8 @@ export const set: { (key: string, value: string) => (self: UrlParams) => UrlParams, (self: UrlParams, key: string, value: string) => UrlParams >(3, (self, key, value) => - Chunk.append( - Chunk.filter(self, ([k]) => k !== key), + ReadonlyArray.append( + ReadonlyArray.filter(self, ([k]) => k !== key), [key, value] )) @@ -64,9 +62,9 @@ export const setAll: { (self: UrlParams, input: Input) => UrlParams >(2, (self, input) => { const toSet = fromInput(input) - const keys = Chunk.toReadonlyArray(toSet).map(([k]) => k) - return Chunk.appendAll( - Chunk.filter(self, ([k]) => keys.includes(k)), + const keys = toSet.map(([k]) => k) + return ReadonlyArray.appendAll( + ReadonlyArray.filter(self, ([k]) => keys.includes(k)), toSet ) }) @@ -82,7 +80,7 @@ export const append: { (key: string, value: string) => (self: UrlParams) => UrlParams, (self: UrlParams, key: string, value: string) => UrlParams >(3, (self, key, value) => - Chunk.append( + ReadonlyArray.append( self, [key, value] )) @@ -98,7 +96,7 @@ export const appendAll: { (input: Input) => (self: UrlParams) => UrlParams, (self: UrlParams, input: Input) => UrlParams >(2, (self, input) => - Chunk.appendAll( + ReadonlyArray.appendAll( self, fromInput(input) )) @@ -113,13 +111,13 @@ export const remove: { } = dual< (key: string) => (self: UrlParams) => UrlParams, (self: UrlParams, key: string) => UrlParams ->(2, (self, key) => Chunk.filter(self, ([k]) => k !== key)) +>(2, (self, key) => ReadonlyArray.filter(self, ([k]) => k !== key)) /** * @since 1.0.0 * @category combinators */ -export const toString = (self: UrlParams): string => new URLSearchParams(Chunk.toReadonlyArray(self) as any).toString() +export const toString = (self: UrlParams): string => new URLSearchParams(self as any).toString() /** * @since 1.0.0 @@ -129,7 +127,7 @@ export const makeUrl = (url: string, params: UrlParams, onError: (e: unknown) Effect.try({ try: () => { const urlInstance = new URL(url, baseUrl()) - Chunk.forEach(params, ([key, value]) => { + ReadonlyArray.forEach(params, ([key, value]) => { if (value !== undefined) { urlInstance.searchParams.append(key, value) } diff --git a/packages/platform/src/internal/http/platform.ts b/packages/platform/src/internal/http/platform.ts index 7f87f5d3..8e1e183c 100644 --- a/packages/platform/src/internal/http/platform.ts +++ b/packages/platform/src/internal/http/platform.ts @@ -4,6 +4,7 @@ import { pipe } from "effect/Function" import * as FileSystem from "../../FileSystem.js" import type * as Body from "../../Http/Body.js" import * as Etag from "../../Http/Etag.js" +import * as Headers from "../../Http/Headers.js" import type * as Platform from "../../Http/Platform.js" import type * as ServerResponse from "../../Http/ServerResponse.js" @@ -19,7 +20,7 @@ export const make = (impl: { path: string, status: number, statusText: string | undefined, - headers: Record, + headers: Headers.Headers, start: number, end: number | undefined, contentLength: number @@ -28,7 +29,7 @@ export const make = (impl: { file: Body.Body.FileLike, status: number, statusText: string | undefined, - headers: Record, + headers: Headers.Headers, options?: FileSystem.StreamOptions ) => ServerResponse.ServerResponse }): Effect.Effect => @@ -45,12 +46,9 @@ export const make = (impl: { Effect.map(({ etag, info }) => { const start = Number(options?.offset ?? 0) const end = options?.bytesToRead !== undefined ? start + Number(options.bytesToRead) : undefined - const headers: Record = { - ...(options?.headers ?? {}), - etag: Etag.toString(etag) - } + const headers = Headers.set(options?.headers ?? Headers.empty, "etag", Etag.toString(etag)) if (info.mtime._tag === "Some") { - headers["last-modified"] = info.mtime.value.toUTCString() + ;(headers as any)["last-modified"] = info.mtime.value.toUTCString() } const contentLength = end !== undefined ? end - start : Number(info.size) - start return impl.fileResponse( @@ -67,11 +65,13 @@ export const make = (impl: { }, fileWebResponse(file, options) { return Effect.map(etagGen.fromFileWeb(file), (etag) => { - const headers: Record = { - ...(options?.headers ?? {}), - etag: Etag.toString(etag), - "last-modified": new Date(file.lastModified).toUTCString() - } + const headers = Headers.merge( + options?.headers ?? Headers.empty, + Headers.unsafeFromRecord({ + etag: Etag.toString(etag), + "last-modified": new Date(file.lastModified).toUTCString() + }) + ) return impl.fileWebResponse( file, options?.status ?? 200,