/** * Builds one-way conversions used by schemas. * * A `Getter` receives an optional encoded value and returns an * optional decoded value. It can also report a schema issue or require Effect * services. Schema transformations use getters to describe one direction of a * conversion, for example decoding a field from input data. This module * includes basic getters, validation helpers, pure and effectful conversions, * and ready-made conversions for common string, number, binary, date, form, and * URL-related values. * * @stability stable * @since 4.0.0 */ import * as Arr from "./Array.ts" import * as DateTime from "./DateTime.ts" import * as Effect from "./Effect.ts" import * as Base64 from "./encoding/Base64.ts" import * as Base64Url from "./encoding/Base64Url.ts" import * as Hex from "./encoding/Hex.ts" import { dual } from "./Function.ts" import * as InternalRecord from "./internal/record.ts" import * as Option from "./Option.ts" import * as Pipeable from "./Pipeable.ts" import * as Predicate from "./Predicate.ts" import * as Result from "./Result.ts" import type * as Schema from "./Schema.ts" import type * as SchemaAST from "./SchemaAST.ts" import * as SchemaIssue from "./SchemaIssue.ts" import * as Str from "./String.ts" /** * A transformation that returns its input unchanged. * * @stability stable * @category models * @since 4.0.0 */ export interface Passthrough extends Pipeable.Pipeable { readonly _tag: "Passthrough" } /** * A synchronous transformation of present values. * * @stability stable * @category models * @since 4.0.0 */ export interface Transform extends Pipeable.Pipeable { readonly _tag: "Transform" readonly transform: (input: E) => T } /** * A synchronous transformation of optional values. * * @stability stable * @category models * @since 4.0.0 */ export interface TransformOptional extends Pipeable.Pipeable { readonly _tag: "TransformOptional" readonly transform: (input: Option.Option) => Option.Option } /** * An effectful transformation of present values. * * @stability stable * @category models * @since 4.0.0 */ export interface TransformEffect extends Pipeable.Pipeable { readonly _tag: "TransformEffect" readonly transform: ( input: E, options: SchemaAST.ParseOptions ) => Effect.Effect } /** * An effectful transformation of optional values. * * @stability stable * @category models * @since 4.0.0 */ export interface TransformOptionalEffect extends Pipeable.Pipeable { readonly _tag: "TransformOptionalEffect" readonly transform: ( input: Option.Option, options: SchemaAST.ParseOptions ) => Effect.Effect, SchemaIssue.Issue, R> } /** * Represents a composable transformation from an encoded type `E` to a decoded type `T`. * * **When to use** * * Use when you need a schema getter to build and compose custom transformations * for `Schema.decodeTo` or `Schema.decode`. * * **Details** * * A getter receives an `Option` and produces an `Option`. `Option.none()` * represents a missing struct field and can also omit a field from the output. * A getter may fail with a schema issue or require services through `R`. The * tagged representation distinguishes synchronous transformations from * transformations that return an `Effect`, allowing schema parsers to select * the corresponding execution path when they are built. Getter values expose * `pipe`; use the standalone {@link map}, {@link compose}, and {@link run} * functions to operate on them. * * **Example** (Creating and composing getters) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const parseNumber = SchemaGetter.transform((s) => Number(s)) * const double = SchemaGetter.transform((n) => n * 2) * const composed = SchemaGetter.compose(parseNumber, double) * Effect.runSync(SchemaGetter.run(composed, Option.some("21"), {})) // => Option.some(42) * ``` * * @see {@link passthrough} for the identity getter * @see {@link transform} to create a getter from a pure function * @see {@link transformEffect} for effectful transformation * * @stability stable * @category models * @since 4.0.0 */ export type Getter = | Passthrough | Transform | TransformOptional | TransformEffect | TransformOptionalEffect const runGetter = ( self: Getter, input: Option.Option, options: SchemaAST.ParseOptions ): Effect.Effect, SchemaIssue.Issue, R> => { switch (self._tag) { case "Passthrough": return Effect.succeed(input as unknown as Option.Option) case "Transform": return Effect.succeed(Option.map(input, self.transform)) case "TransformOptional": return Effect.succeed(self.transform(input)) case "TransformEffect": return Option.isNone(input) ? Effect.succeedNone : Effect.mapEager(self.transform(input.value, options), Option.some) case "TransformOptionalEffect": return self.transform(input, options) } } /** * Runs a getter directly. * * **Details** * * This is a convenience API for executing a getter outside a schema. When the * getter belongs to a schema, use the corresponding `SchemaParser` API. The * result is an `Effect` for every getter variant, including synchronous ones. * * **Example** (Running a getter) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const getter = SchemaGetter.transform(Number) * * const result = Effect.runSync( * SchemaGetter.run(getter, Option.some("42"), {}) * ) * result // => Option.some(42) * ``` * * @stability stable * @category converting * @since 4.0.0 */ export const run: { /** * Runs a getter directly. * * **Details** * * This is a convenience API for executing a getter outside a schema. When the * getter belongs to a schema, use the corresponding `SchemaParser` API. The * result is an `Effect` for every getter variant, including synchronous ones. * * **Example** (Running a getter) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const getter = SchemaGetter.transform(Number) * * const result = Effect.runSync( * SchemaGetter.run(getter, Option.some("42"), {}) * ) * result // => Option.some(42) * ``` * * @stability stable * @category converting * @since 4.0.0 */ (input: Option.Option, options: SchemaAST.ParseOptions): (self: Getter) => Effect.Effect, SchemaIssue.Issue, R> /** * Runs a getter directly. * * **Details** * * This is a convenience API for executing a getter outside a schema. When the * getter belongs to a schema, use the corresponding `SchemaParser` API. The * result is an `Effect` for every getter variant, including synchronous ones. * * **Example** (Running a getter) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const getter = SchemaGetter.transform(Number) * * const result = Effect.runSync( * SchemaGetter.run(getter, Option.some("42"), {}) * ) * result // => Option.some(42) * ``` * * @stability stable * @category converting * @since 4.0.0 */ ( self: Getter, input: Option.Option, options: SchemaAST.ParseOptions ): Effect.Effect, SchemaIssue.Issue, R> } = dual(3, runGetter) const makeGetter = (fields: A): A & Pipeable.Pipeable => Object.assign(Object.create(Pipeable.Prototype), fields) const composeOptionalEffect = ( first: Getter, second: Getter ): Getter => transformOptionalEffect((input, options) => Effect.flatMapEager(runGetter(first, input, options), (output) => runGetter(second, output, options)) ) /** * Composes two getters by passing the output of the first to the second. * * **When to use** * * Use when a schema conversion requires multiple transformation steps. * * **Details** * * Composition forwards `Option.none()` to getters that handle optional values * and skips getters that operate only on present values. Composing with * {@link passthrough} returns the other getter unchanged. The function supports * both `compose(first, second)` and `first.pipe(compose(second))`. * * **Example** (Parsing and normalizing a number) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const getter = SchemaGetter.compose( * SchemaGetter.transform(Number), * SchemaGetter.transform((n) => Math.max(0, n)) * ) * * Effect.runSync(SchemaGetter.run(getter, Option.some("-1"), {})) // => Option.some(0) * ``` * * @stability stable * @category combining * @since 4.0.0 */ export const compose: { /** * Composes two getters by passing the output of the first to the second. * * **When to use** * * Use when a schema conversion requires multiple transformation steps. * * **Details** * * Composition forwards `Option.none()` to getters that handle optional values * and skips getters that operate only on present values. Composing with * {@link passthrough} returns the other getter unchanged. The function supports * both `compose(first, second)` and `first.pipe(compose(second))`. * * **Example** (Parsing and normalizing a number) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const getter = SchemaGetter.compose( * SchemaGetter.transform(Number), * SchemaGetter.transform((n) => Math.max(0, n)) * ) * * Effect.runSync(SchemaGetter.run(getter, Option.some("-1"), {})) // => Option.some(0) * ``` * * @stability stable * @category combining * @since 4.0.0 */ (other: Getter): (self: Getter) => Getter /** * Composes two getters by passing the output of the first to the second. * * **When to use** * * Use when a schema conversion requires multiple transformation steps. * * **Details** * * Composition forwards `Option.none()` to getters that handle optional values * and skips getters that operate only on present values. Composing with * {@link passthrough} returns the other getter unchanged. The function supports * both `compose(first, second)` and `first.pipe(compose(second))`. * * **Example** (Parsing and normalizing a number) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const getter = SchemaGetter.compose( * SchemaGetter.transform(Number), * SchemaGetter.transform((n) => Math.max(0, n)) * ) * * Effect.runSync(SchemaGetter.run(getter, Option.some("-1"), {})) // => Option.some(0) * ``` * * @stability stable * @category combining * @since 4.0.0 */ (self: Getter, other: Getter): Getter } = dual(2, ( self: Getter, other: Getter ): Getter => { if (self._tag === "Passthrough") return other as Getter if (other._tag === "Passthrough") return self as unknown as Getter switch (self._tag) { case "Transform": { switch (other._tag) { case "Transform": return transform((input: E) => other.transform(self.transform(input))) case "TransformOptional": return transformOptional((input) => other.transform(Option.map(input, self.transform))) case "TransformEffect": return transformEffect((input: E, options) => other.transform(self.transform(input), options)) case "TransformOptionalEffect": return composeOptionalEffect(self, other) } } case "TransformOptional": { switch (other._tag) { case "Transform": return transformOptional((input) => Option.map(self.transform(input), other.transform)) case "TransformOptional": return transformOptional((input) => other.transform(self.transform(input))) case "TransformEffect": case "TransformOptionalEffect": return composeOptionalEffect(self, other) } } case "TransformEffect": { switch (other._tag) { case "Transform": return transformEffect((input: E, options) => Effect.mapEager(self.transform(input, options), other.transform) ) case "TransformOptional": case "TransformOptionalEffect": return composeOptionalEffect(self, other) case "TransformEffect": return transformEffect((input: E, options) => Effect.flatMapEager(self.transform(input, options), (output) => other.transform(output, options)) ) } } case "TransformOptionalEffect": return composeOptionalEffect(self, other) } }) /** * Maps the output of a getter while preserving missing values. * * **When to use** * * Use to add a synchronous transformation after an existing getter. * * **Details** * * The mapping function runs only for `Option.some`. The function supports both * `map(self, f)` and `self.pipe(map(f))`. * * **Example** (Mapping a getter result) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const getter = SchemaGetter.transform(Number).pipe( * SchemaGetter.map((n) => n * 2) * ) * * Effect.runSync(SchemaGetter.run(getter, Option.some("21"), {})) // => Option.some(42) * ``` * * @stability stable * @category mapping * @since 4.0.0 */ export const map: { /** * Maps the output of a getter while preserving missing values. * * **When to use** * * Use to add a synchronous transformation after an existing getter. * * **Details** * * The mapping function runs only for `Option.some`. The function supports both * `map(self, f)` and `self.pipe(map(f))`. * * **Example** (Mapping a getter result) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const getter = SchemaGetter.transform(Number).pipe( * SchemaGetter.map((n) => n * 2) * ) * * Effect.runSync(SchemaGetter.run(getter, Option.some("21"), {})) // => Option.some(42) * ``` * * @stability stable * @category mapping * @since 4.0.0 */ (f: (value: T) => T2): (self: Getter) => Getter /** * Maps the output of a getter while preserving missing values. * * **When to use** * * Use to add a synchronous transformation after an existing getter. * * **Details** * * The mapping function runs only for `Option.some`. The function supports both * `map(self, f)` and `self.pipe(map(f))`. * * **Example** (Mapping a getter result) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const getter = SchemaGetter.transform(Number).pipe( * SchemaGetter.map((n) => n * 2) * ) * * Effect.runSync(SchemaGetter.run(getter, Option.some("21"), {})) // => Option.some(42) * ``` * * @stability stable * @category mapping * @since 4.0.0 */ (self: Getter, f: (value: T) => T2): Getter } = dual(2, (self: Getter, f: (value: T) => T2): Getter => compose(self, transform(f))) /** * Creates a getter that always produces the given constant value, ignoring the input. * * **When to use** * * Use when you need a schema getter that always decodes a field to a fixed * value. * * **Details** * * The getter is pure and always returns `Option.some(t)` regardless of whether * the input is `Some` or `None`. * * **Example** (Returning a constant getter) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const alwaysZero = SchemaGetter.succeed(0) * Effect.runSync(SchemaGetter.run(alwaysZero, Option.none(), {})) // => Option.some(0) * ``` * * @see {@link transform} when you need to use the input value * @see {@link passthrough} when you want to keep the input as-is * * @stability stable * @category constructors * @since 4.0.0 */ export function succeed(t: T): Getter { return transformOptional(() => Option.some(t)) } /** * Creates a getter that always fails with the given issue. * * **When to use** * * Use when you need a schema getter that unconditionally rejects input. * - Building custom validation getters that produce specific error types. * * **Details** * * - Always fails with the `Issue` returned by `f`. * - The failure function receives the original `Option` input and the * effective `ParseOptions` for error context. * * **Example** (Defining an always-failing getter) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter, SchemaIssue } from "effect" * * const rejectAll = SchemaGetter.fail( * () => new SchemaIssue.InvalidValue({ message: "not allowed" }) * ) * const issue = await Effect.runPromise( * Effect.flip(SchemaGetter.run(rejectAll, Option.some("x"), {})) * ) * issue._tag // => "InvalidValue" * ``` * * @see {@link forbidden} for a convenience helper for `Forbidden` issues * @see {@link checkEffect} to fail conditionally based on input value * * @stability stable * @category constructors * @since 4.0.0 */ export function fail( f: (oe: Option.Option, options: SchemaAST.ParseOptions) => SchemaIssue.Issue ): Getter { return transformOptionalEffect((oe, options) => Effect.fail(f(oe, options))) } /** * Creates a getter that always fails with a `Forbidden` issue. * * **When to use** * * Use when you need a schema getter to disallow a field or direction * (encode/decode) entirely. * - You want a clear "forbidden" error message in schema validation output. * * **Details** * * - Always fails with `SchemaIssue.Forbidden`. * - The message function receives the `Option` input for context. * * **Example** (Forbidding a decode direction) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const noEncode = SchemaGetter.forbidden( * () => "encoding is not supported" * ) * const issue = await Effect.runPromise( * Effect.flip(SchemaGetter.run(noEncode, Option.some(1), {})) * ) * issue._tag // => "Forbidden" * ``` * * @see {@link fail} to fail with a custom issue type * * @stability stable * @category constructors * @since 4.0.0 */ export function forbidden(message: (oe: Option.Option) => string): Getter { return fail((oe, options) => { const annotations = { message: message(oe) } return Option.isSome(oe) ? new SchemaIssue.Forbidden(annotations, oe.value, options) : new SchemaIssue.Forbidden(annotations) }) } /** * Getter that always fails with a `Forbidden` issue indicating that encoding is unsupported. * * **When to use** * * Use as the encode side of a decode-only Schema transformation. * * **Details** * * Its `Getter` type is assignable to every encoding getter because it accepts any input and never * produces an output value. * * **Example** (Rejecting encoding) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const issue = await Effect.runPromise( * Effect.flip(SchemaGetter.run(SchemaGetter.forbiddenEncoding, Option.some("value"), {})) * ) * issue._tag // => "Forbidden" * ``` * * @see {@link forbidden} for a forbidden getter with a custom message * * @stability stable * @category constructors * @since 4.0.0 */ export const forbiddenEncoding: Getter = forbidden(() => "Encoding is not supported") const passthrough_: Passthrough = makeGetter({ _tag: "Passthrough" }) /** * Returns the identity getter — passes the value through unchanged. * * **When to use** * * Use when you need a schema getter for one side of a `decodeTo` pair, either * encode or decode, to pass values through unchanged. * * **Details** * * - Pure, no allocation (singleton instance). * - Optimized away by {@link compose} — composing with a passthrough is free. * - The default overload requires `T === E`. Pass `{ strict: false }` to opt * out of the type constraint. * * **Example** (Passing through identity transformations) * * ```ts import.meta.vitest * import { Schema, SchemaGetter } from "effect" * * // No transformation needed — types already match * const StringToString = Schema.String.pipe( * Schema.decodeTo(Schema.String, { * decode: SchemaGetter.passthrough(), * encode: SchemaGetter.passthrough() * }) * ) * Schema.decodeSync(StringToString)("hello") // => "hello" * ``` * * @see {@link passthroughSupertype} when `T extends E` * @see {@link passthroughSubtype} when `E extends T` * @see {@link transform} when you need to change the value * * @stability stable * @category constructors * @since 4.0.0 */ export function passthrough(options: { readonly strict: false }): Getter export function passthrough(): Getter export function passthrough(): Getter { return passthrough_ } /** * Returns the identity getter typed for the relationship `T extends E`. * * **When to use** * * Use when you need a schema getter that passes values through when the * decoded/output type is narrower than the encoded/input type. * * **Details** * * - Same singleton as {@link passthrough} — no allocation, optimized in composition. * * **Example** (Passing through supertypes) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * // string extends string, so this is valid * const g = SchemaGetter.passthroughSupertype() * Effect.runSync(SchemaGetter.run(g, Option.some("hello"), {})) // => Option.some("hello") * ``` * * @see {@link passthrough} when types are identical * @see {@link passthroughSubtype} when `E extends T` * * @stability stable * @category constructors * @since 4.0.0 */ export function passthroughSupertype(): Getter export function passthroughSupertype(): Getter { return passthrough_ } /** * Returns the identity getter, typed for when the encoded type `E` is a subtype of `T`. * * **When to use** * * Use when you need a schema getter that passes values through without * `{ strict: false }` for an encoded type that narrows the decoded type. * * **Details** * * - Same singleton as {@link passthrough} — no allocation, optimized in composition. * * **Example** (Passing through subtypes) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * // "hello" extends string, so E extends T * const g = SchemaGetter.passthroughSubtype() * Effect.runSync(SchemaGetter.run(g, Option.some("hello"), {})) // => Option.some("hello") * ``` * * @see {@link passthrough} when types are identical * @see {@link passthroughSupertype} when `T extends E` * * @stability stable * @category constructors * @since 4.0.0 */ export function passthroughSubtype(): Getter export function passthroughSubtype(): Getter { return passthrough_ } /** * Creates a getter that fails with `MissingKey` if the input is absent (`Option.None`). * * **When to use** * * Use when you need a schema getter to require a struct field in the encoded * input and report a missing key error when it is absent. * * **Details** * * - When input is `None`, fails with `SchemaIssue.MissingKey`. * - When input is `Some`, passes it through unchanged. * - Optional `annotations` customize the error message for the missing key. * * **Example** (Defining a required struct field) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const mustExist = SchemaGetter.required() * const issue = await Effect.runPromise( * Effect.flip(SchemaGetter.run(mustExist, Option.none(), {})) * ) * issue._tag // => "MissingKey" * ``` * * @see {@link withDefault} to substitute a default for undefined values * * @stability stable * @category validation * @since 4.0.0 */ export function required(annotations?: Schema.Annotations.Key): Getter { return transformOptionalEffect((input) => Option.isNone(input) ? Effect.fail(new SchemaIssue.MissingKey(annotations)) : Effect.succeed(input) ) } /** * Creates a getter that validates a value using an effectful check function. * * **When to use** * * Use when you need a schema getter to validate a decoded value (e.g. check a * constraint or call an external service). * - The validation may be asynchronous or require Effect services. * * **Details** * * - Only runs when input is `Some` — `None` passes through. * - The check function returns a validation result: * - `undefined` or `true` — value is valid, passes through. * - `false` or a `string` — value is invalid, fails with an `Issue`. * - An `Issue` object — fails with that issue directly. * - `{ path, issue }` — fails with a nested path issue (`issue` may be a * message string or a full {@link SchemaIssue.Issue}). * - Does not transform the value — input and output types are the same. * * **Example** (Validating effectfully) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const nonNegative = SchemaGetter.checkEffect((n) => * Effect.succeed(n >= 0 ? undefined : "must be non-negative") * ) * await Effect.runPromise(SchemaGetter.run(nonNegative, Option.some(1), {})) // => Option.some(1) * ``` * * @see {@link transform} when you need to change the value, not just validate * @see {@link fail} for unconditional failure * * @stability stable * @category validation * @since 4.0.0 */ export function checkEffect( f: (input: T, options: SchemaAST.ParseOptions) => Effect.Effect< undefined | boolean | Schema.FilterIssue, never, R > ): Getter { return transformEffect((t, options) => { return f(t, options).pipe(Effect.flatMapEager((out) => { const issue = SchemaIssue.makeSingle(out, t, options) return issue ? Effect.fail(issue) : Effect.succeed(t) })) }) } /** * Creates a getter that applies a pure function to present values. * * **When to use** * * Use when you need a schema getter for a pure, infallible transformation * between types. * - Building encode/decode pairs for `Schema.decodeTo`. * * **Details** * * - This is the most commonly used constructor. * - Transforms `Some(e)` to `Some(f(e))` and leaves `None` unchanged. * - Skips `None` inputs — only called when a value is present. * - Never fails. * * **Example** (Transforming strings to numbers) * * ```ts import.meta.vitest * import { Schema, SchemaGetter } from "effect" * * const NumberFromString = Schema.String.pipe( * Schema.decodeTo(Schema.Number, { * decode: SchemaGetter.transform((s) => Number(s)), * encode: SchemaGetter.transform((n) => String(n)) * }) * ) * Schema.decodeSync(NumberFromString)("42") // => 42 * ``` * * @see {@link transformEffect} when the transformation returns an `Effect` * @see {@link transformOptional} when you need to handle `None` inputs * @see {@link passthrough} when no transformation is needed * * @stability stable * @category transforming * @since 4.0.0 */ export function transform(f: (e: E) => T): Getter { return makeGetter({ _tag: "Transform", transform: f }) } /** * Creates a getter that applies an effectful transformation to present values. * * **When to use** * * Use when you need a schema getter for a transformation that may fail, require * Effect services, or run asynchronously. * * **Details** * * - Skips `None` inputs — only called when a value is present. * - On success, wraps the result in `Some`. * - On failure, propagates the `Issue`. * * **Example** (Parsing with failure) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter, SchemaIssue } from "effect" * * const safeParseInt = SchemaGetter.transformEffect( * (s, options) => { * const n = parseInt(s, 10) * return isNaN(n) * ? Effect.fail(new SchemaIssue.InvalidValue({ message: "not an integer" }, s, options)) * : Effect.succeed(n) * } * ) * await Effect.runPromise(SchemaGetter.run(safeParseInt, Option.some("42"), {})) // => Option.some(42) * ``` * * @see {@link transform} when transformation cannot fail * @see {@link transformOptionalEffect} when you need full `Option` control over the output * * @stability stable * @category transforming * @since 4.0.0 */ export function transformEffect( f: (e: E, options: SchemaAST.ParseOptions) => Effect.Effect ): Getter { return makeGetter({ _tag: "TransformEffect", transform: f }) } /** * Creates a getter that transforms the full `Option` — both present and absent values. * * **When to use** * * Use when you need a schema getter to handle both `Some` and `None` cases. * * **Details** * * The getter is pure and never fails. It receives the full `Option` and * must return `Option`, so it can turn a present value into absent or an * absent value into present. * * **Example** (Filtering out empty strings) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const skipEmpty = SchemaGetter.transformOptional((o) => * Option.filter(o, (s) => s.length > 0) * ) * Effect.runSync(SchemaGetter.run(skipEmpty, Option.some(""), {})) // => Option.none() * ``` * * @see {@link transform} when you only need to transform present values * @see {@link omit} when you always want `None` * * @stability stable * @category transforming * @since 4.0.0 */ export function transformOptional(f: (oe: Option.Option) => Option.Option): Getter { return makeGetter({ _tag: "TransformOptional", transform: f }) } /** * Creates a getter that effectfully transforms the full `Option`. * * @stability stable * @category transforming * @since 4.0.0 */ export function transformOptionalEffect( f: ( input: Option.Option, options: SchemaAST.ParseOptions ) => Effect.Effect, SchemaIssue.Issue, R> ): Getter { return makeGetter({ _tag: "TransformOptionalEffect", transform: f }) } /** * Creates a getter that always returns `None`, effectively omitting the value from output. * * **When to use** * * Use when you need a schema getter to exclude a field during decoding or * encoding. * * **Details** * * - Always returns `Option.None` regardless of input. * - Never fails. * * **Example** (Omitting a field during encoding) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const omitField = SchemaGetter.omit() * Effect.runSync(SchemaGetter.run(omitField, Option.some("hidden"), {})) // => Option.none() * ``` * * @see {@link transformOptional} when you want conditional omission * @see {@link forbidden} when you want to fail instead of silently omit * * @stability stable * @category filtering * @since 4.0.0 */ export function omit(): Getter { return transformOptional(() => Option.none()) } /** * Creates a getter that replaces `undefined` values with a default. * * **When to use** * * Use when you need a schema getter to provide a fallback for a field that may * be `undefined` in the encoded input. * * **Details** * * - If the input is `Some(undefined)` or `None`, produces `Some(T)`. * - If the input is `Some(value)` where value is not `undefined`, passes it through. * - `defaultValue` is an `Effect` that will be executed each time a default is needed. * * **Example** (Providing a default value for an optional field) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const withZero = SchemaGetter.withDefault(Effect.succeed(0)) * await Effect.runPromise(SchemaGetter.run(withZero, Option.some(undefined), {})) // => Option.some(0) * ``` * * @see {@link transformOptionalEffect} for custom effectful missing-key handling * @see {@link required} when absent input should fail instead of using a default * * @stability stable * @category transforming * @since 4.0.0 */ export function withDefault( defaultValue: Effect.Effect ): Getter { return transformOptionalEffect((o) => { const filtered = Option.filter(o, Predicate.isNotUndefined) return Option.isSome(filtered) ? Effect.succeed(filtered) : Effect.mapEager(defaultValue, Option.some) }) } /** * Coerces any value to a `string` using the global `String()` constructor. * * **When to use** * * Use when you need a schema getter to coerce a present encoded value to a * string with `String()`. * * **Details** * * The getter is pure, never fails, and delegates to `globalThis.String`. * * **Example** (Coercing to a string) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const toString = SchemaGetter.String() * Effect.runSync(SchemaGetter.run(toString, Option.some(42), {})) // => Option.some("42") * ``` * * @see {@link transform} for custom string conversions * * @stability stable * @category converting * @since 4.0.0 */ export function String(): Getter { return transform(globalThis.String) } /** * Coerces any value to a `number` using the global `Number()` constructor. * * **When to use** * * Use when you need a schema getter to coerce a present encoded value to a * number with `Number()`. * * **Details** * * The getter is pure, never fails, and delegates to `globalThis.Number`. It may * produce `NaN` for non-numeric inputs. * * **Example** (Coercing to a number) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const toNumber = SchemaGetter.Number() * Effect.runSync(SchemaGetter.run(toNumber, Option.some("42"), {})) // => Option.some(42) * ``` * * @see {@link transformEffect} for effectful or validated number parsing * * @stability stable * @category converting * @since 4.0.0 */ export function Number(): Getter { return transform(globalThis.Number) } /** * Coerces any value to a `boolean` using the global `Boolean()` constructor. * * **When to use** * * Use when you need a schema getter to coerce a present encoded value to a * boolean with `Boolean()`. * * **Details** * * The getter is pure, never fails, and delegates to `globalThis.Boolean`. * * **Example** (Coercing to a boolean) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const toBool = SchemaGetter.Boolean() * Effect.runSync(SchemaGetter.run(toBool, Option.some("true"), {})) // => Option.some(true) * ``` * * @stability stable * @category converting * @since 4.0.0 */ export function Boolean(): Getter { return transform(globalThis.Boolean) } /** * Coerces a value to `bigint` using the global `BigInt()` constructor. * * **When to use** * * Use when you need a schema getter to convert a present string, number, or * boolean value to `bigint`. * * **Details** * * - Delegates to `globalThis.BigInt`. * - Fails with `SchemaIssue.InvalidValue` if the input cannot be converted * (e.g. a non-numeric string or a non-integer number). * * **Example** (Coercing to a bigint) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const toBigInt = SchemaGetter.BigInt() * Effect.runSync(SchemaGetter.run(toBigInt, Option.some("42"), {})) // => Option.some(42n) * ``` * * @stability stable * @category converting * @since 4.0.0 */ export function BigInt(): Getter { return transformEffect((input, options) => { try { return Effect.succeed(globalThis.BigInt(input)) } catch { return Effect.fail( new SchemaIssue.InvalidValue( { expected: "a value that can be converted to a bigint" }, input, options ) ) } }) } /** * Coerces a value to a `Date` using `new Date(input)`. * * **When to use** * * Use when you need a schema getter to coerce a present string, number, or * existing date object into a new date object. * * **Details** * * - Delegates to `new globalThis.Date(input)`. * - Does not validate the result — may produce an invalid Date. * * **Example** (Coercing to a Date) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const toDate = SchemaGetter.Date() * const result = Effect.runSync(SchemaGetter.run(toDate, Option.some("1970-01-01"), {})) * Option.map(result, (date) => date.toISOString()) // => Option.some("1970-01-01T00:00:00.000Z") * ``` * * @see {@link dateTimeUtcFromInput} for validated DateTime parsing * * @stability stable * @category converting * @since 4.0.0 */ export function Date(): Getter { return transform((u) => new globalThis.Date(u)) } /** * Strips whitespace from both ends of a string. * * **Details** * * - Pure, delegates to `String.trim`. * * **Example** (Trimming whitespace) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const trimmed = SchemaGetter.trim() * Effect.runSync(SchemaGetter.run(trimmed, Option.some(" hello "), {})) // => Option.some("hello") * ``` * * @stability stable * @category transforming * @since 4.0.0 */ export function trim(): Getter { return transform(Str.trim) } /** * Capitalizes the first character of a string. * * **Details** * * - Pure, delegates to `String.capitalize`. * * **Example** (Capitalizing a string) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const cap = SchemaGetter.capitalize() * Effect.runSync(SchemaGetter.run(cap, Option.some("hello"), {})) // => Option.some("Hello") * ``` * * @stability stable * @category transforming * @since 4.0.0 */ export function capitalize(): Getter { return transform(Str.capitalize) } /** * Uncapitalizes the first character of a string. * * **Details** * * - Pure, delegates to `String.uncapitalize`. * * **Example** (Uncapitalizing a string) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const uncap = SchemaGetter.uncapitalize() * Effect.runSync(SchemaGetter.run(uncap, Option.some("Hello"), {})) // => Option.some("hello") * ``` * * @stability stable * @category transforming * @since 4.0.0 */ export function uncapitalize(): Getter { return transform(Str.uncapitalize) } /** * Converts a `snake_case` string to `camelCase`. * * **Details** * * - Pure, delegates to `String.snakeToCamel`. * * **Example** (Converting snake case to camel case) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const toCamel = SchemaGetter.snakeToCamel() * Effect.runSync(SchemaGetter.run(toCamel, Option.some("user_name"), {})) // => Option.some("userName") * ``` * * @see {@link camelToSnake} for the inverse operation * * @stability stable * @category transforming * @since 4.0.0 */ export function snakeToCamel(): Getter { return transform(Str.snakeToCamel) } /** * Converts a `camelCase` string to `snake_case`. * * **Details** * * - Pure, delegates to `String.camelToSnake`. * * **Example** (Converting camel case to snake case) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const toSnake = SchemaGetter.camelToSnake() * Effect.runSync(SchemaGetter.run(toSnake, Option.some("userName"), {})) // => Option.some("user_name") * ``` * * @see {@link snakeToCamel} for the inverse operation * * @stability stable * @category transforming * @since 4.0.0 */ export function camelToSnake(): Getter { return transform(Str.camelToSnake) } /** * Converts a string to lowercase. * * **Details** * * - Pure, delegates to `String.toLowerCase`. * * **Example** (Converting to lowercase) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const lower = SchemaGetter.toLowerCase() * Effect.runSync(SchemaGetter.run(lower, Option.some("HELLO"), {})) // => Option.some("hello") * ``` * * @see {@link toUpperCase} for the inverse operation * * @stability stable * @category transforming * @since 4.0.0 */ export function toLowerCase(): Getter { return transform(Str.toLowerCase) } /** * Converts a string to uppercase. * * **Details** * * - Pure, delegates to `String.toUpperCase`. * * **Example** (Converting to uppercase) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const upper = SchemaGetter.toUpperCase() * Effect.runSync(SchemaGetter.run(upper, Option.some("hello"), {})) // => Option.some("HELLO") * ``` * * @see {@link toLowerCase} for the inverse operation * * @stability stable * @category transforming * @since 4.0.0 */ export function toUpperCase(): Getter { return transform(Str.toUpperCase) } type ParseJsonOptions = { readonly reviver?: Parameters[1] } /** * Parses a JSON string into a value. * * **When to use** * * Use when you need a schema getter to parse a present encoded JSON string * during decoding. * * **Details** * * - Skips `None` inputs. * - Without `reviver`: returns `Schema.MutableJson` (typed JSON). * - With `reviver`: returns `unknown` (reviver may produce arbitrary values). * - On parse failure, fails with `SchemaIssue.InvalidValue` whose `expected` * annotation is `"a valid JSON string"`. Its default message includes the * reported input when `reportInput` is enabled. * * **Example** (Parsing JSON) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const parse = SchemaGetter.parseJson() * const result = await Effect.runPromise(SchemaGetter.run(parse, Option.some("{\"a\":1}"), {})) * result // => Option.some({ a: 1 }) * ``` * * @see {@link stringifyJson} for the inverse operation * * @stability stable * @category decoding * @since 4.0.0 */ export function parseJson(): Getter export function parseJson(options: ParseJsonOptions): Getter export function parseJson(options?: ParseJsonOptions | undefined): Getter { return transformEffect((input, parseOptions) => Effect.try({ try: () => JSON.parse(input, options?.reviver), catch: () => new SchemaIssue.InvalidValue( { expected: "a valid JSON string" }, input, parseOptions ) }) ) } /** * Replacer function or property allowlist accepted by `JSON.stringify`. * * @stability stable * @category utility types * @since 4.0.0 */ export type JsonReplacer = | ((this: any, key: string, value: any) => any) | Array | null type StringifyJsonOptions = { readonly replacer?: JsonReplacer | undefined readonly space?: Parameters[2] } /** * Stringifies a present value using `JSON.stringify`. * * **When to use** * * Use when you need a schema getter to serialize a present decoded value to * JSON text during encoding. * * **Details** * * - Skips `None` inputs. * - If `JSON.stringify` throws or returns `undefined`, fails with * `SchemaIssue.InvalidValue`. * - Supports optional `replacer` and `space` options, matching * `JSON.stringify`. * * **Example** (Stringifying JSON) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const stringify = SchemaGetter.stringifyJson() * const result = await Effect.runPromise(SchemaGetter.run(stringify, Option.some({ a: 1 }), {})) * result // => Option.some("{\"a\":1}") * ``` * * @see {@link parseJson} for the inverse operation * * @stability stable * @category encoding * @since 4.0.0 */ export function stringifyJson(options?: StringifyJsonOptions): Getter { return transformEffect((input, parseOptions) => Effect.try({ try: () => { const output = JSON.stringify(input, options?.replacer as any, options?.space) if (output === undefined) { throw new TypeError("Value cannot be represented as JSON") } return output }, catch: () => new SchemaIssue.InvalidValue( { expected: "a JSON-serializable value" }, input, parseOptions ) }) ) } /** * Parses a string into a record of key-value pairs. * * **When to use** * * Use when you need a schema getter to parse a present encoded string that * contains delimited key-value pairs (e.g. `"a=1,b=2"`). * * **Details** * * The getter is pure and never fails. It splits the string by `separator` * (default `,`) and then each pair at the first `keyValueSeparator` (default `=`), * preserving the rest of the value. An empty `keyValueSeparator` uses the first * two characters as the key and value. Pairs missing a key or value are skipped. * * **Example** (Parsing a key-value string) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const parse = SchemaGetter.splitKeyValue() * const result = Effect.runSync(SchemaGetter.run(parse, Option.some("a=1,b=2"), {})) * result // => Option.some({ a: "1", b: "2" }) * ``` * * @see {@link joinKeyValue} for the inverse operation * @see {@link split} to split into an array of strings * * @stability stable * @category splitting * @since 4.0.0 */ export function splitKeyValue(options?: { readonly separator?: string | undefined readonly keyValueSeparator?: string | undefined }): Getter, E> { const separator = options?.separator ?? "," const keyValueSeparator = options?.keyValueSeparator ?? "=" return transform((input) => input.split(separator).reduce((acc, pair) => { if (keyValueSeparator === "") { const [key, value] = pair.split("") if (key && value) InternalRecord.assignProperty(acc, key, value) return acc } const index = pair.indexOf(keyValueSeparator) if (index <= 0) return acc const key = pair.slice(0, index) const value = pair.slice(index + keyValueSeparator.length) if (value) { InternalRecord.assignProperty(acc, key, value) } return acc }, {} as Record) ) } /** * Joins a record of key-value pairs into a delimited string. * * **When to use** * * Use when you need a schema getter to serialize a present decoded record as a * delimited key-value string. * * **Details** * * The getter is pure and never fails. It joins entries with `separator` * (default `,`) and joins each key and value with `keyValueSeparator` (default * `=`). * * **Example** (Joining key-value records) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const join = SchemaGetter.joinKeyValue() * const result = Effect.runSync(SchemaGetter.run(join, Option.some({ a: "1", b: "2" }), {})) * result // => Option.some("a=1,b=2") * ``` * * @see {@link splitKeyValue} for the inverse operation * * @stability stable * @category combining * @since 4.0.0 */ export function joinKeyValue>(options?: { readonly separator?: string | undefined readonly keyValueSeparator?: string | undefined }): Getter { const separator = options?.separator ?? "," const keyValueSeparator = options?.keyValueSeparator ?? "=" return transform((input) => Object.entries(input).map(([key, value]) => `${key}${keyValueSeparator}${value}`).join(separator) ) } /** * Splits a string into an array of strings by a separator. * * **When to use** * * Use when you need a schema getter to split a present encoded string * containing a delimited list, such as CSV values. * * **Details** * * The getter is pure and never fails. It splits by `separator` (default `,`). * An empty string produces an empty array, not `[""]`. * * **Example** (Splitting a comma-separated string) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const splitComma = SchemaGetter.split() * const result = Effect.runSync(SchemaGetter.run(splitComma, Option.some("a,b,c"), {})) * result // => Option.some(["a", "b", "c"]) * ``` * * @see {@link splitKeyValue} when values are key-value pairs * * @stability stable * @category splitting * @since 4.0.0 */ export function split(options?: { readonly separator?: string | undefined }): Getter, E> { const separator = options?.separator ?? "," return transform((input) => input === "" ? [] : input.split(separator)) } /** * Encodes a `Uint8Array` or string to a Base64 string. * * **Details** * * The getter is pure and never fails. * * **Example** (Encoding to Base64) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const encode = SchemaGetter.encodeBase64() * const result = Effect.runSync(SchemaGetter.run(encode, Option.some(new Uint8Array([1, 2, 3])), {})) * result // => Option.some("AQID") * ``` * * @see {@link decodeBase64} for the inverse operation to `Uint8Array` * @see {@link decodeBase64String} for the inverse operation to `string` * @see {@link encodeBase64Url} for the URL-safe variant * * @stability stable * @category encoding * @since 4.0.0 */ export function encodeBase64(): Getter { return transform(Base64.encode) } /** * Encodes a `Uint8Array` or string to a URL-safe Base64 string. * * **Details** * * The getter is pure and never fails. The output is unpadded unless * `padding` is `true`. * * **Example** (Encoding to Base64Url) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const encode = SchemaGetter.encodeBase64Url() * const result = Effect.runSync(SchemaGetter.run(encode, Option.some(new Uint8Array([251, 255])), {})) * result // => Option.some("-_8") * ``` * * @see {@link decodeBase64Url} for the inverse operation to `Uint8Array` * @see {@link decodeBase64UrlString} for the inverse operation to `string` * @see {@link encodeBase64} for the standard Base64 variant * * @stability stable * @category encoding * @since 4.0.0 */ export function encodeBase64Url(options?: { /** Keep the `=` padding. Defaults to `false`. */ readonly padding?: boolean | undefined }): Getter { return transform(options?.padding ? Base64Url.encodePadded : Base64Url.encode) } /** * Encodes a `Uint8Array` or string to a hexadecimal string. * * **Details** * * The getter is pure and never fails. * * **Example** (Encoding to hex) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const encode = SchemaGetter.encodeHex() * const result = Effect.runSync(SchemaGetter.run(encode, Option.some(new Uint8Array([1, 2, 3])), {})) * result // => Option.some("010203") * ``` * * @see {@link decodeHex} for the inverse operation to `Uint8Array` * @see {@link decodeHexString} for the inverse operation to `string` * * @stability stable * @category encoding * @since 4.0.0 */ export function encodeHex(): Getter { return transform(Hex.encode) } /** * Decodes a Base64 string to a `Uint8Array`. * * **Details** * * - Fails with `SchemaIssue.InvalidValue` if the input is not valid Base64. * * **Example** (Decoding Base64 to bytes) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const decode = SchemaGetter.decodeBase64() * const result = await Effect.runPromise(SchemaGetter.run(decode, Option.some("AQID"), {})) * Option.map(result, Array.from) // => Option.some([1, 2, 3]) * ``` * * @see {@link decodeBase64String} to decode to `string` instead * @see {@link encodeBase64} for the inverse operation * * @stability stable * @category decoding * @since 4.0.0 */ export function decodeBase64(): Getter { return transformEffect((input, options) => Effect.mapErrorEager( Effect.fromResult(Base64.decode(input)), () => new SchemaIssue.InvalidValue( { expected: "a valid Base64 string" }, input, options ) ) ) } /** * Decodes a Base64 string to a UTF-8 `string`. * * **Details** * * - Fails with `SchemaIssue.InvalidValue` if the input is not valid Base64. * * **Example** (Decoding Base64 to string) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const decode = SchemaGetter.decodeBase64String() * const result = await Effect.runPromise(SchemaGetter.run(decode, Option.some("aGVsbG8="), {})) * result // => Option.some("hello") * ``` * * @see {@link decodeBase64} to decode to `Uint8Array` instead * @see {@link encodeBase64} for the inverse operation * * @stability stable * @category decoding * @since 4.0.0 */ export function decodeBase64String(): Getter { return transformEffect((input, options) => Result.match(Base64.decodeString(input), { onFailure: () => Effect.fail( new SchemaIssue.InvalidValue( { expected: "a valid Base64 string" }, input, options ) ), onSuccess: Effect.succeed }) ) } /** * Decodes a URL-safe Base64 string to a `Uint8Array`. * * **Details** * * - Fails with `SchemaIssue.InvalidValue` if the input is not valid Base64Url. * * **Example** (Decoding Base64Url to bytes) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const decode = SchemaGetter.decodeBase64Url() * const result = await Effect.runPromise(SchemaGetter.run(decode, Option.some("-_8="), {})) * Option.map(result, Array.from) // => Option.some([251, 255]) * ``` * * @see {@link decodeBase64UrlString} to decode to `string` instead * @see {@link encodeBase64Url} for the inverse operation * * @stability stable * @category decoding * @since 4.0.0 */ export function decodeBase64Url(): Getter { return transformEffect((input, options) => Result.match(Base64Url.decode(input), { onFailure: () => Effect.fail( new SchemaIssue.InvalidValue( { expected: "a valid Base64Url string" }, input, options ) ), onSuccess: Effect.succeed }) ) } /** * Decodes a URL-safe Base64 string to a UTF-8 `string`. * * **Details** * * - Fails with `SchemaIssue.InvalidValue` if the input is not valid Base64Url. * * **Example** (Decoding Base64Url to string) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const decode = SchemaGetter.decodeBase64UrlString() * const result = await Effect.runPromise(SchemaGetter.run(decode, Option.some("aGVsbG8"), {})) * result // => Option.some("hello") * ``` * * @see {@link decodeBase64Url} to decode to `Uint8Array` instead * @see {@link encodeBase64Url} for the inverse operation * * @stability stable * @category decoding * @since 4.0.0 */ export function decodeBase64UrlString(): Getter { return transformEffect((input, options) => Result.match(Base64Url.decodeString(input), { onFailure: () => Effect.fail( new SchemaIssue.InvalidValue( { expected: "a valid Base64Url string" }, input, options ) ), onSuccess: Effect.succeed }) ) } /** * Decodes a hexadecimal string to a `Uint8Array`. * * **Details** * * - Fails with `SchemaIssue.InvalidValue` if the input is not valid hex. * * **Example** (Decoding hex to bytes) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const decode = SchemaGetter.decodeHex() * const result = await Effect.runPromise(SchemaGetter.run(decode, Option.some("010203"), {})) * Option.map(result, Array.from) // => Option.some([1, 2, 3]) * ``` * * @see {@link decodeHexString} to decode to `string` instead * @see {@link encodeHex} for the inverse operation * * @stability stable * @category decoding * @since 4.0.0 */ export function decodeHex(): Getter { return transformEffect((input, options) => Result.match(Hex.decode(input), { onFailure: () => Effect.fail( new SchemaIssue.InvalidValue( { expected: "a valid hexadecimal string" }, input, options ) ), onSuccess: Effect.succeed }) ) } /** * Decodes a hexadecimal string to a UTF-8 `string`. * * **Details** * * - Fails with `SchemaIssue.InvalidValue` if the input is not valid hex. * * **Example** (Decoding hex to string) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const decode = SchemaGetter.decodeHexString() * const result = await Effect.runPromise(SchemaGetter.run(decode, Option.some("68656c6c6f"), {})) * result // => Option.some("hello") * ``` * * @see {@link decodeHex} to decode to `Uint8Array` instead * @see {@link encodeHex} for the inverse operation * * @stability stable * @category decoding * @since 4.0.0 */ export function decodeHexString(): Getter { return transformEffect((input, options) => Result.match(Hex.decodeString(input), { onFailure: () => Effect.fail( new SchemaIssue.InvalidValue( { expected: "a valid hexadecimal string" }, input, options ) ), onSuccess: Effect.succeed }) ) } /** * Encodes a present string using `encodeURIComponent`. * * **Details** * * - Skips `None` inputs. * - May throw a `URIError` for malformed surrogate pairs; this exception is not * converted into an `Issue`. * * **Example** (Encoding a URI component) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const encode = SchemaGetter.encodeUriComponent() * const result = Effect.runSync(SchemaGetter.run(encode, Option.some("hello world"), {})) * result // => Option.some("hello%20world") * ``` * * @see {@link decodeUriComponent} for the inverse operation * * @stability stable * @category encoding * @since 4.0.0 */ export function encodeUriComponent(): Getter { return transform(encodeURIComponent) } /** * Decodes a URI component encoded string using `decodeURIComponent`. * * **Details** * * - Fails with `SchemaIssue.InvalidValue` if the input contains malformed percent-encoding sequences. * * **Example** (Decoding a URI component) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const decode = SchemaGetter.decodeUriComponent() * const result = await Effect.runPromise(SchemaGetter.run(decode, Option.some("hello%20world"), {})) * result // => Option.some("hello world") * ``` * * @see {@link encodeUriComponent} for the inverse operation * * @stability stable * @category decoding * @since 4.0.0 */ export function decodeUriComponent(): Getter { return transformEffect((input, options) => { try { return Effect.succeed(globalThis.decodeURIComponent(input)) } catch { return Effect.fail( new SchemaIssue.InvalidValue( { expected: "a valid URI component" }, input, options ) ) } }) } /** * Parses a `DateTime.Input` value into a `DateTime.Utc`. * * **When to use** * * Use when you need a schema getter to decode a present encoded date/time value * to a `DateTime.Utc`. * * **Details** * * - Accepted input includes existing `DateTime` values, partial date/time parts, * instant objects, zoned instant objects, JavaScript `Date` instances, epoch * milliseconds, and date strings. * - Converts successfully parsed values to UTC. * - Fails with `SchemaIssue.InvalidValue` if the input cannot be parsed as a valid * `DateTime`. * * **Example** (Parsing DateTime) * * ```ts import.meta.vitest * import { DateTime, Effect, Option, SchemaGetter } from "effect" * * const parseDate = SchemaGetter.dateTimeUtcFromInput() * const result = await Effect.runPromise( * SchemaGetter.run(parseDate, Option.some("2024-01-01T00:00:00Z"), {}) * ) * Option.map(result, DateTime.toEpochMillis) // => Option.some(1704067200000) * ``` * * @see {@link Date} for a simpler coercion to `Date` (no validation) * * @stability stable * @category converting * @since 4.0.0 */ export function dateTimeUtcFromInput(): Getter { return transformEffect((input, options) => { return Option.match(DateTime.make(input), { onNone: () => Effect.fail( new SchemaIssue.InvalidValue({ message: "Invalid DateTime input" }, input, options) ), onSome: (dt) => Effect.succeed(DateTime.toUtc(dt)) }) }) } /** * Decodes a `FormData` object into a nested tree structure using bracket-path notation. * * **When to use** * * Use when you need a schema getter to parse `FormData` from HTTP requests into * structured objects. * * **Details** * * The getter is pure and never fails. It interprets bracket-path keys such as * `user[name]` and `items[0]` to build nested objects or arrays, and each leaf * value is a `string` or `Blob`. * * **Example** (Decoding FormData) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const decode = SchemaGetter.decodeFormData() * const formData = new FormData() * formData.append("user[name]", "Alice") * const result = Effect.runSync(SchemaGetter.run(decode, Option.some(formData), {})) * result // => Option.some({ user: { name: "Alice" } }) * ``` * * @see {@link encodeFormData} for the corresponding encoder * @see {@link makeTreeRecord} for the underlying bracket-path parser * @see {@link decodeURLSearchParams} for the URLSearchParams variant * * @stability stable * @category decoding * @since 4.0.0 */ export function decodeFormData(): Getter, FormData> { return transform((input) => makeTreeRecord(Array.from(input.entries()))) } const collectFormDataEntries = collectBracketPathEntries((value): value is string | Blob => typeof value === "string" || (typeof Blob !== "undefined" && value instanceof Blob) ) /** * Encodes a nested object into a `FormData` instance using bracket-path notation. * * **When to use** * * Use when you need a schema getter to serialize structured data to `FormData` * for HTTP requests. * * **Details** * * The getter is pure and never fails. It flattens nested objects or arrays into * bracket-path keys such as `user[name]` and `items[0]`. Non-object inputs * produce an empty `FormData`. * * **Example** (Encoding to FormData) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const encode = SchemaGetter.encodeFormData() * const result = Effect.runSync(SchemaGetter.run(encode, Option.some({ name: "Alice" }), {})) * Option.map(result, (formData) => formData.get("name")) // => Option.some("Alice") * ``` * * @see {@link decodeFormData} for the corresponding decoder * @see {@link collectBracketPathEntries} for the underlying flattener * @see {@link encodeURLSearchParams} for the URLSearchParams variant * * @stability stable * @category encoding * @since 4.0.0 */ export function encodeFormData(): Getter { return transform((input) => { const out = new FormData() if (typeof input === "object" && input !== null) { const entries = collectFormDataEntries(input) entries.forEach(([key, value]) => { out.append(key, value) }) } return out }) } /** * Decodes a `URLSearchParams` object into a nested tree structure using bracket-path notation. * * **When to use** * * Use when you need a schema getter to parse query parameters from URLs into * structured objects. * * **Details** * * The getter is pure and never fails. It interprets bracket-path keys such as * `user[name]` and `items[0]` to build nested objects or arrays, and each leaf * value is a `string`. * * **Example** (Decoding URLSearchParams) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const decode = SchemaGetter.decodeURLSearchParams() * const params = new URLSearchParams("user[name]=Alice") * const result = Effect.runSync(SchemaGetter.run(decode, Option.some(params), {})) * result // => Option.some({ user: { name: "Alice" } }) * ``` * * @see {@link encodeURLSearchParams} for the corresponding encoder * @see {@link makeTreeRecord} for the underlying bracket-path parser * @see {@link decodeFormData} for the FormData variant * * @stability stable * @category decoding * @since 4.0.0 */ export function decodeURLSearchParams(): Getter, URLSearchParams> { return transform((input) => makeTreeRecord(Array.from(input.entries()))) } const collectURLSearchParamsEntries = collectBracketPathEntries(Predicate.isString) /** * Encodes a nested object into a `URLSearchParams` instance using bracket-path notation. * * **When to use** * * Use when you need a schema getter to serialize structured data to query * parameters for URLs. * * **Details** * * The getter is pure and never fails. It flattens nested objects or arrays into * bracket-path keys. Non-object inputs produce an empty `URLSearchParams`. * * **Example** (Encoding to URLSearchParams) * * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const encode = SchemaGetter.encodeURLSearchParams() * const result = Effect.runSync(SchemaGetter.run(encode, Option.some({ name: "Alice" }), {})) * Option.map(result, (params) => params.toString()) // => Option.some("name=Alice") * ``` * * @see {@link decodeURLSearchParams} for the corresponding decoder * @see {@link collectBracketPathEntries} for the underlying flattener * @see {@link encodeFormData} for the FormData variant * * @stability stable * @category encoding * @since 4.0.0 */ export function encodeURLSearchParams(): Getter { return transform((input) => { if (typeof input === "object" && input !== null) { return new URLSearchParams(collectURLSearchParamsEntries(input)) } return new URLSearchParams() }) } function bracketPathToTokens(bracketPath: string): Array { // real empty path (from append("", value)) if (bracketPath === "") { return [""] } const replaced = bracketPath.replace(/\[(.*?)\]/g, ".$1") const parts = replaced.split(".") // if bracket path started with "[...]" we get ".foo" => ["", "foo"]; drop the synthetic first "" const start = replaced.startsWith(".") ? 1 : 0 return parts .slice(start) .map((part) => (Arr.isCanonicalArrayIndex(part) ? globalThis.Number(part) : part)) } const MAX_ARRAY_HOLES = 20 /** * Builds a nested tree object from a list of bracket-path entries. * * **When to use** * * Use when you need a schema getter to parse FormData or URLSearchParams * entries into structured objects. * - You have flat key-value pairs with bracket-path keys that need nesting. * * **Details** * * - A bracket path is a string like `"user[address][city]"` that describes nested * object/array structure. * - Interprets bracket paths and constructs the corresponding nested object. * - Builds and returns a nested object from the input entries. * - Supported syntax: * - `"foo"` → object key `"foo"` * - `"foo[bar]"` → nested `{ foo: { bar: ... } }` * - `"foo[0]"` → array index `{ foo: [value] }` * - `"foo[]"` → append to array `foo` * - `""` → real empty key * - Numeric bracket segments become array indices only when they are valid * JavaScript array-index strings; otherwise they remain object keys. * - An indexed container becomes an array only when it has at most 20 holes * (missing indices below its highest index). Otherwise it is returned as an * object keyed by the index strings, so untrusted input such as * `a[4294967294]` cannot produce huge sparse arrays. * - Duplicate keys for the same path are merged into arrays. * - If a structural path conflicts with a previous leaf or a different container * type, the later structural path replaces the conflicting value. * - The notation has no escaping for `.`, `[` or `]`, so keys containing these * delimiters cannot be round-tripped without changing their structure. * * **Example** (Building a tree from bracket paths) * * ```ts import.meta.vitest * import { SchemaGetter } from "effect" * * SchemaGetter.makeTreeRecord([ * ["user[name]", "Alice"], * ["user[tags][]", "admin"], * ["user[tags][]", "editor"] * ]) // => { user: { name: "Alice", tags: ["admin", "editor"] } } * ``` * * @see {@link collectBracketPathEntries} for flattening trees into bracket-path entries * @see {@link decodeFormData} for a higher-level FormData decoder * @see {@link decodeURLSearchParams} for a higher-level URLSearchParams decoder * * @stability stable * @category constructors * @since 4.0.0 */ export function makeTreeRecord( bracketPathEntries: ReadonlyArray ): Schema.TreeRecord { const out: any = {} const containers = new WeakSet() const duplicates = new WeakSet() // Indexed containers are built as plain objects so that large indices never // allocate sparse arrays, and are materialized once all entries are applied const indexed = new Map() bracketPathEntries.forEach(([key, value]) => { const tokens = bracketPathToTokens(key) let cur: any = out for (let i = 0; i < tokens.length; i++) { const state = indexed.get(cur) // Resolve "[]" to the next index before handling leaves or containers const token = tokens[i] === "" && state !== undefined ? state.length : tokens[i] if (state !== undefined && typeof token === "number") { state.length = Math.max(state.length, token + 1) } const hasOwn = Object.hasOwn(cur, token) const current = hasOwn ? cur[token] : undefined if (i === tokens.length - 1) { const currentState = indexed.get(current) if (currentState !== undefined) { InternalRecord.assignProperty(current, currentState.length++, value) } else if (duplicates.has(current)) { current.push(value) } else if (hasOwn) { const values = [current, value] duplicates.add(values) InternalRecord.assignProperty(cur, token, values) } else { InternalRecord.assignProperty(cur, token, value) } } else { const next = tokens[i + 1] const shouldBeArray = typeof next === "number" || next === "" if (containers.has(current) && indexed.has(current) === shouldBeArray) { cur = current } else { const container = {} containers.add(container) if (shouldBeArray) { indexed.set(container, { parent: cur, key: token, length: 0 }) } InternalRecord.assignProperty(cur, token, container) cur = container } } } }) // Children are created after their parents, so materializing in reverse // creation order replaces every nested container before its parent is copied for (const [builder, { key, length, parent }] of Array.from(indexed).reverse()) { // A later conflicting path may have replaced the container in its parent if (parent[key] === builder && length - Object.keys(builder).length <= MAX_ARRAY_HOLES) { InternalRecord.assignProperty(parent, key, Object.assign(new Array(length), builder)) } } return out } /** * Flattens a nested object into bracket-path entries, filtering leaf values by a type guard. * * **When to use** * * Use when you need a schema getter to serialize structured objects to flat * key-value entries. * - Building custom `FormData` or `URLSearchParams` encoders. * * **Details** * * - Takes a nested object and produces flat `[bracketPath, value]` pairs suitable for * `FormData` or `URLSearchParams`. * - Returns a curried function: first call provides the leaf type guard, second call provides the object. * - Recursively traverses objects and arrays. * - If all elements of an array are leaves, encodes them as multiple entries with the same key * (e.g. `tags=a&tags=b`). Otherwise uses indexed bracket paths (e.g. `items[0]`, `items[1]`). * - Non-leaf values that aren't objects or arrays are silently skipped. * - Empty arrays and objects produce no entries, and path delimiters in property * names are not escaped. The resulting format is therefore lossy. * * **Example** (Flattening an object to bracket paths) * * ```ts import.meta.vitest * import { Predicate, SchemaGetter } from "effect" * * const collectStrings = SchemaGetter.collectBracketPathEntries(Predicate.isString) * const entries = collectStrings({ user: { name: "Alice", tags: ["admin", "editor"] } }) * * entries // => [["user[name]", "Alice"], ["user[tags]", "admin"], ["user[tags]", "editor"]] * ``` * * @see {@link makeTreeRecord} for building trees from bracket-path entries * @see {@link encodeFormData} for a higher-level FormData encoder * @see {@link encodeURLSearchParams} for a higher-level URLSearchParams encoder * * @stability stable * @category converting * @since 4.0.0 */ export function collectBracketPathEntries(isLeaf: (value: unknown) => value is A) { return (input: object): Array<[bracketPath: string, value: A]> => { const bracketPathEntries: Array<[string, A]> = [] function append(key: string, value: unknown): void { if (isLeaf(value)) { bracketPathEntries.push([key, value]) } else if (Array.isArray(value)) { // If all values are leaves, encode as multiple entries with the same key const allLeaves = value.every(isLeaf) if (allLeaves) { value.forEach((v) => { bracketPathEntries.push([key, v]) }) } else { value.forEach((v, i) => { append(`${key}[${i}]`, v) }) } } else if (typeof value === "object" && value !== null) { for (const [k, v] of Object.entries(value)) { append(`${key}[${k}]`, v) } } } for (const [key, value] of Object.entries(input)) { append(key, value) } return bracketPathEntries } }