/** * Composable transformation primitives for the Effect Schema system. * * A `Getter` represents a single-direction transformation from an * encoded type `E` to a decoded type `T`. Getters are the building blocks * that `Schema.decodeTo` and `Schema.decode` use to define how values are * transformed during encoding and decoding. They handle optionality * (`Option` in, `Option` out), can fail with `Issue`, and can require * Effect services via `R`. * * ## Mental model * * - **Getter**: A function `Option -> Effect, Issue, R>`. It * transforms an optional encoded value into an optional decoded value, * possibly failing or requiring services. * - **Passthrough**: The identity getter — returns the input unchanged. Used * when no transformation is needed. Optimized away during composition. * - **Option-awareness**: Getters receive and return `Option` to handle * missing keys in structs. `Option.None` means the key is absent. * - **Composition**: Getters compose left-to-right via `.compose()`. A * passthrough on either side is a no-op (identity optimization). * - **Issue**: The error type for all getter failures (see `SchemaIssue`). * * ## Common tasks * * - Pass a value through unchanged → {@link passthrough} * - Transform a value purely → {@link transform} * - Transform a value with possible failure → {@link transformOrFail} * - Transform with full Option control → {@link transformOptional} * - Handle missing keys → {@link onNone}, {@link required}, {@link withDefault} * - Handle present values → {@link onSome} * - Validate a value with an effectful check → {@link checkEffect} * - Produce a constant value → {@link succeed} * - Always fail → {@link fail}, {@link forbidden} * - Omit a value from output → {@link omit} * - Coerce to a primitive type → {@link String}, {@link Number}, {@link Boolean}, {@link BigInt}, {@link Date} * - Transform strings → {@link trim}, {@link capitalize}, {@link toLowerCase}, {@link toUpperCase}, {@link split}, {@link splitKeyValue}, {@link joinKeyValue} * - Parse/stringify JSON → {@link parseJson}, {@link stringifyJson} * - Encode/decode Base64 → {@link encodeBase64}, {@link decodeBase64}, {@link decodeBase64String} * - Encode/decode Hex → {@link encodeHex}, {@link decodeHex}, {@link decodeHexString} * - Encode/decode URI components → {@link encodeUriComponent}, {@link decodeUriComponent} * - Parse DateTime → {@link dateTimeUtcFromInput} * - Decode/encode FormData → {@link decodeFormData}, {@link encodeFormData} * - Decode/encode URLSearchParams → {@link decodeURLSearchParams}, {@link encodeURLSearchParams} * - Build nested tree from bracket paths → {@link makeTreeRecord} * - Flatten nested tree to bracket paths → {@link collectBracketPathEntries} * * ## Gotchas * * - Getters are not bidirectional. To define a full encode/decode pair, supply * both a `decode` and an `encode` getter to `Schema.decodeTo`. * - `passthrough` requires `T === E` by default. Use `{ strict: false }` to * bypass the type constraint, or use {@link passthroughSupertype} / {@link passthroughSubtype}. * - `transform` skips `None` inputs (missing keys) — the function is only * called when a value is present. Use `transformOptional` if you need to * handle missing values. * - `parseJson` without a `reviver` returns `Schema.MutableJson`. With a * reviver, the return type widens to `unknown`. * - `split` treats an empty string as an empty array, not `[""]`. * * ## Quickstart * * **Example** (Using SchemaGetter with Schema.decodeTo) * * ```ts * 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)) * }) * ) * * const result = Schema.decodeUnknownSync(NumberFromString)("42") * // result: 42 * ``` * * ## See also * * - {@link Getter} — the core class * - {@link transform} — most common constructor * - {@link passthrough} — identity getter * - {@link transformOrFail} — fallible transformation * * @since 4.0.0 */ import * as DateTime from "./DateTime.ts" import * as Effect from "./Effect.ts" import * as Encoding from "./Encoding.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 AST from "./SchemaAST.ts" import * as Issue from "./SchemaIssue.ts" import * as Str from "./String.ts" /** * A composable transformation from an encoded type `E` to a decoded type `T`. * * A Getter wraps a function `Option -> Effect, Issue, R>`: * - Receives `Option.None` when the encoded key is absent (e.g. missing struct field). * - Returns `Option.None` to omit the value from the decoded output. * - Fails with `Issue` on invalid input. * - May require Effect services via `R`. * * Use this when: * - Building custom schema transformations with `Schema.decodeTo` or `Schema.decode`. * - Composing multiple transformation steps into a single getter. * * Behavior: * - Immutable — constructing or composing getters does not mutate existing instances. * - `.map(f)` applies `f` to the decoded value (inside the `Some`), leaving `None` unchanged. * - `.compose(other)` chains two getters: the output of `this` feeds into `other`. * Passthrough getters on either side are optimized away. * * **Example** (Creating and composing getters) * * ```ts * import { SchemaGetter } from "effect" * * const parseNumber = SchemaGetter.transform((s) => Number(s)) * const double = SchemaGetter.transform((n) => n * 2) * const composed = parseNumber.compose(double) * // composed: Getter — parses then doubles * ``` * * See also: * - {@link transform} — create a getter from a pure function * - {@link passthrough} — identity getter * - {@link transformOrFail} — fallible transformation * * @category model * @since 4.0.0 */ export class Getter extends Pipeable.Class { readonly run: ( input: Option.Option, options: AST.ParseOptions ) => Effect.Effect, Issue.Issue, R> constructor( run: ( input: Option.Option, options: AST.ParseOptions ) => Effect.Effect, Issue.Issue, R> ) { super() this.run = run } map(f: (t: T) => T2): Getter { return new Getter((oe, options) => this.run(oe, options).pipe(Effect.mapEager(Option.map(f)))) } compose(other: Getter): Getter { if (isPassthrough(this)) { return other as any } if (isPassthrough(other)) { return this as any } return new Getter((oe, options) => this.run(oe, options).pipe(Effect.flatMapEager((ot) => other.run(ot, options)))) } } /** * Creates a getter that always produces the given constant value, ignoring the input. * * Use this when: * - A schema field should always decode to a fixed value. * - You need a placeholder getter that produces a known default. * * Behavior: * - Pure, no side effects. * - Always returns `Option.some(t)` regardless of whether input is `Some` or `None`. * * **Example** (Constant getter) * * ```ts * import { SchemaGetter } from "effect" * * const alwaysZero = SchemaGetter.succeed(0) * // alwaysZero: Getter<0, unknown> — always produces 0 * ``` * * See also: * - {@link transform} — when you need to use the input value * - {@link passthrough} — when you want to keep the input as-is * * @category Constructors * @since 4.0.0 */ export function succeed(t: T): Getter { return new Getter(() => Effect.succeedSome(t)) } /** * Creates a getter that always fails with the given issue. * * Use this when: * - A transformation should unconditionally reject input. * - Building custom validation getters that produce specific error types. * * Behavior: * - Always fails with the `Issue` returned by `f`. * - The failure function receives the original `Option` input for error context. * * **Example** (Always-failing getter) * * ```ts * import { SchemaGetter, SchemaIssue, Option } from "effect" * * const rejectAll = SchemaGetter.fail( * (oe) => new SchemaIssue.InvalidValue(oe, { message: "not allowed" }) * ) * ``` * * See also: * - {@link forbidden} — convenience for `Forbidden` issues * - {@link checkEffect} — fail conditionally based on input value * * @category Constructors * @since 4.0.0 */ export function fail(f: (oe: Option.Option) => Issue.Issue): Getter { return new Getter((oe) => Effect.fail(f(oe))) } /** * Creates a getter that always fails with a `Forbidden` issue. * * Use this when: * - A field or direction (encode/decode) should be disallowed entirely. * - You want a clear "forbidden" error message in schema validation output. * * Behavior: * - Always fails with `Issue.Forbidden`. * - The message function receives the `Option` input for context. * * **Example** (Forbidding a decode direction) * * ```ts * import { SchemaGetter } from "effect" * * const noEncode = SchemaGetter.forbidden( * () => "encoding is not supported" * ) * ``` * * See also: * - {@link fail} — fail with a custom issue type * * @category Constructors * @since 4.0.0 */ export function forbidden(message: (oe: Option.Option) => string): Getter { return fail((oe) => new Issue.Forbidden(oe, { message: message(oe) })) } const passthrough_ = new Getter(Effect.succeed) function isPassthrough(getter: Getter): getter is typeof passthrough_ { return getter.run === passthrough_.run } /** * Returns the identity getter — passes the value through unchanged. * * Use this when: * - No transformation is needed between encoded and decoded types. * - One side of a `decodeTo` pair (encode or decode) should be a no-op. * * Behavior: * - Pure, no allocation (singleton instance). * - Optimized away during `.compose()` — composing with a passthrough is free. * - The default overload requires `T === E`. Pass `{ strict: false }` to opt * out of the type constraint. * * **Example** (Identity transformation) * * ```ts * 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() * }) * ) * ``` * * See also: * - {@link passthroughSupertype} — when `T extends E` * - {@link passthroughSubtype} — when `E extends T` * - {@link transform} — when you need to change the value * * @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 when the decoded type `T` is a supertype of `E`. * * Use this when: * - The decoded type is wider than the encoded type (e.g. `string` from a string literal). * - You need type-safe passthrough without `{ strict: false }`. * * Behavior: * - Same singleton as {@link passthrough} — no allocation, optimized in composition. * * **Example** (Supertype passthrough) * * ```ts * import { SchemaGetter } from "effect" * * // string extends string, so this is valid * const g = SchemaGetter.passthroughSupertype() * ``` * * See also: * - {@link passthrough} — when types are identical * - {@link passthroughSubtype} — when `E extends T` * * @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`. * * Use this when: * - The encoded type is narrower than the decoded type. * - You need type-safe passthrough without `{ strict: false }`. * * Behavior: * - Same singleton as {@link passthrough} — no allocation, optimized in composition. * * **Example** (Subtype passthrough) * * ```ts * import { SchemaGetter } from "effect" * * // "hello" extends string, so E extends T * const g = SchemaGetter.passthroughSubtype() * ``` * * See also: * - {@link passthrough} — when types are identical * - {@link passthroughSupertype} — when `T extends E` * * @category Constructors * @since 4.0.0 */ export function passthroughSubtype(): Getter export function passthroughSubtype(): Getter { return passthrough_ } /** * Creates a getter that handles the case when the input is absent (`Option.None`). * * Use this when: * - You need to provide a fallback or computed value for missing struct keys. * - Building custom "default value" logic more complex than {@link withDefault}. * * Behavior: * - When input is `None`, calls `f` to produce the result. * - When input is `Some`, passes it through unchanged. * - `f` receives the parse options and may return `None` to keep the value absent. * * **Example** (Default timestamp for missing field) * * ```ts * import { SchemaGetter, Effect, Option } from "effect" * * const withTimestamp = SchemaGetter.onNone(() => * Effect.succeed(Option.some(Date.now())) * ) * ``` * * See also: * - {@link required} — fails if input is absent * - {@link withDefault} — simpler default value for undefined inputs * - {@link onSome} — handle only present values * * @category Constructors * @since 4.0.0 */ export function onNone( f: (options: AST.ParseOptions) => Effect.Effect, Issue.Issue, R> ): Getter { return new Getter((ot, options) => Option.isNone(ot) ? f(options) : Effect.succeed(ot)) } /** * Creates a getter that fails with `MissingKey` if the input is absent (`Option.None`). * * Use this when: * - A struct field must be present in the encoded input. * - You want schema validation to report a missing key error. * * Behavior: * - When input is `None`, fails with `Issue.MissingKey`. * - When input is `Some`, passes it through unchanged. * - Optional `annotations` customize the error message for the missing key. * * **Example** (Required struct field) * * ```ts * import { SchemaGetter } from "effect" * * const mustExist = SchemaGetter.required() * ``` * * See also: * - {@link onNone} — provide a fallback instead of failing * - {@link withDefault} — substitute a default for undefined values * * @category Constructors * @since 4.0.0 */ export function required(annotations?: Schema.Annotations.Key): Getter { return onNone(() => Effect.fail(new Issue.MissingKey(annotations))) } /** * Creates a getter that handles present values (`Option.Some`), passing `None` through. * * Use this when: * - You need to transform or validate only when a value is present. * - Missing keys should remain absent in the output. * * Behavior: * - When input is `None`, returns `None` (no-op). * - When input is `Some(e)`, calls `f(e, options)` to produce the result. * - `f` may return `None` to omit the value, or fail with an `Issue`. * * **Example** (Transform only present values) * * ```ts * import { SchemaGetter, Effect, Option } from "effect" * * const parseIfPresent = SchemaGetter.onSome( * (s) => Effect.succeed(Option.some(Number(s))) * ) * ``` * * See also: * - {@link onNone} — handle only absent values * - {@link transform} — simpler pure transformation of present values * - {@link transformOrFail} — fallible transformation of present values * * @category Constructors * @since 4.0.0 */ export function onSome( f: (e: E, options: AST.ParseOptions) => Effect.Effect, Issue.Issue, R> ): Getter { return new Getter((oe, options) => Option.isNone(oe) ? Effect.succeedNone : f(oe.value, options)) } /** * Creates a getter that validates a value using an effectful check function. * * Use this when: * - You need to validate a decoded value (e.g. check a constraint or call an external service). * - The validation may be asynchronous or require Effect services. * * Behavior: * - 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 Issue.Issue}). * - Does not transform the value — input and output types are the same. * * **Example** (Effectful validation) * * ```ts * import { SchemaGetter, Effect } from "effect" * * const nonNegative = SchemaGetter.checkEffect((n) => * Effect.succeed(n >= 0 ? undefined : "must be non-negative") * ) * ``` * * See also: * - {@link transform} — when you need to change the value, not just validate * - {@link fail} — unconditional failure * * @category Constructors * @since 4.0.0 */ export function checkEffect( f: (input: T, options: AST.ParseOptions) => Effect.Effect< undefined | boolean | Schema.FilterIssue, never, R > ): Getter { return onSome((t, options) => { return f(t, options).pipe(Effect.flatMapEager((out) => { const issue = Issue.makeSingle(t, out) return issue ? Effect.fail(issue) : Effect.succeed(Option.some(t)) })) }) } /** * Creates a getter that applies a pure function to present values. * * This is the most commonly used constructor. It transforms `Some(e)` to * `Some(f(e))` and leaves `None` unchanged. * * Use this when: * - You have a pure, infallible transformation between types. * - Building encode/decode pairs for `Schema.decodeTo`. * * Behavior: * - Pure, does not mutate input. * - Skips `None` inputs — only called when a value is present. * - Never fails. * * **Example** (String to number transformation pair) * * ```ts * 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)) * }) * ) * ``` * * See also: * - {@link transformOrFail} — when the transformation can fail * - {@link transformOptional} — when you need to handle `None` inputs * - {@link passthrough} — when no transformation is needed * * @category Constructors * @since 4.0.0 */ export function transform(f: (e: E) => T): Getter { return transformOptional(Option.map(f)) } /** * Creates a getter that applies a fallible, effectful transformation to present values. * * Use this when: * - The transformation may fail (e.g. parsing, validation). * - The transformation needs Effect services or is async. * * Behavior: * - 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 { SchemaGetter, SchemaIssue, Effect, Option } from "effect" * * const safeParseInt = SchemaGetter.transformOrFail( * (s) => { * const n = parseInt(s, 10) * return isNaN(n) * ? Effect.fail(new SchemaIssue.InvalidValue(Option.some(s), { message: "not an integer" })) * : Effect.succeed(n) * } * ) * ``` * * See also: * - {@link transform} — when transformation cannot fail * - {@link onSome} — when you need full `Option` control over the output * * @category Constructors * @since 4.0.0 */ export function transformOrFail( f: (e: E, options: AST.ParseOptions) => Effect.Effect ): Getter { return onSome((e, options) => f(e, options).pipe(Effect.mapEager(Option.some))) } /** * Creates a getter that transforms the full `Option` — both present and absent values. * * Use this when: * - You need to handle both `Some` and `None` cases. * - You want to turn a present value into absent, or vice versa. * * Behavior: * - Pure, never fails. * - Receives the full `Option` and must return `Option`. * * **Example** (Filter out empty strings) * * ```ts * import { SchemaGetter, Option } from "effect" * * const skipEmpty = SchemaGetter.transformOptional((o) => * Option.filter(o, (s) => s.length > 0) * ) * ``` * * See also: * - {@link transform} — simpler, only handles present values * - {@link omit} — always returns `None` * * @category Constructors * @since 4.0.0 */ export function transformOptional(f: (oe: Option.Option) => Option.Option): Getter { return new Getter((oe) => Effect.succeed(f(oe))) } /** * Creates a getter that always returns `None`, effectively omitting the value from output. * * Use this when: * - A field should be excluded during decoding or encoding. * * Behavior: * - Always returns `Option.None` regardless of input. * - Never fails. * * **Example** (Omit a field during encoding) * * ```ts * import { SchemaGetter } from "effect" * * const omitField = SchemaGetter.omit() * ``` * * See also: * - {@link transformOptional} — when you want conditional omission * - {@link forbidden} — when you want to fail instead of silently omit * * @category Constructors * @since 4.0.0 */ export function omit(): Getter { return new Getter(() => Effect.succeedNone) } /** * Creates a getter that replaces `undefined` values with a default. * * Use this when: * - A field may be `undefined` in the encoded input and should have a fallback. * * Behavior: * - 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** (Default value for optional field) * * ```ts * import { Effect, SchemaGetter } from "effect" * * const withZero = SchemaGetter.withDefault(Effect.succeed(0)) * // Getter * ``` * * See also: * - {@link onNone} — handle only absent keys (not `undefined` values) * - {@link required} — fail instead of providing a default * * @category Constructors * @since 4.0.0 */ export function withDefault(defaultValue: Effect.Effect): Getter { return new Getter((o) => { const filtered = Option.filter(o, Predicate.isNotUndefined) return Option.isSome(filtered) ? Effect.succeed(filtered) : Effect.map(defaultValue, Option.some) }) } /** * Coerces any value to a `string` using the global `String()` constructor. * * Use this when: * - You need a string representation of an arbitrary encoded value. * * Behavior: * - Pure, never fails. * - Delegates to `globalThis.String`. * * **Example** (Coerce to string) * * ```ts * import { SchemaGetter } from "effect" * * const toString = SchemaGetter.String() * // Getter * ``` * * See also: * - {@link transform} — for custom string conversions * * @category Coercions * @since 4.0.0 */ export function String(): Getter { return transform(globalThis.String) } /** * Coerces any value to a `number` using the global `Number()` constructor. * * Use this when: * - You need numeric coercion of an encoded value. * * Behavior: * - Pure, never fails (may produce `NaN` for non-numeric inputs). * - Delegates to `globalThis.Number`. * * **Example** (Coerce to number) * * ```ts * import { SchemaGetter } from "effect" * * const toNumber = SchemaGetter.Number() * // Getter * ``` * * See also: * - {@link transformOrFail} — for validated number parsing * * @category Coercions * @since 4.0.0 */ export function Number(): Getter { return transform(globalThis.Number) } /** * Coerces any value to a `boolean` using the global `Boolean()` constructor. * * Use this when: * - You need boolean coercion (truthiness check) of an encoded value. * * Behavior: * - Pure, never fails. * - Delegates to `globalThis.Boolean`. * * **Example** (Coerce to boolean) * * ```ts * import { SchemaGetter } from "effect" * * const toBool = SchemaGetter.Boolean() * // Getter * ``` * * @category Coercions * @since 4.0.0 */ export function Boolean(): Getter { return transform(globalThis.Boolean) } /** * Coerces a value to `bigint` using the global `BigInt()` constructor. * * Use this when: * - You need to convert strings, numbers, or booleans to `bigint`. * * Behavior: * - Delegates to `globalThis.BigInt`. * - Throws at runtime if the input cannot be converted (e.g. non-numeric string). * * **Example** (Coerce to bigint) * * ```ts * import { SchemaGetter } from "effect" * * const toBigInt = SchemaGetter.BigInt() * // Getter * ``` * * @category Coercions * @since 4.0.0 */ export function BigInt(): Getter { return transform(globalThis.BigInt) } /** * Coerces a value to a `Date` using `new Date(input)`. * * Use this when: * - You need to parse a string, number, or Date into a `Date` object. * * Behavior: * - Delegates to `new globalThis.Date(input)`. * - Does not validate the result — may produce an invalid Date. * * **Example** (Coerce to Date) * * ```ts * import { SchemaGetter } from "effect" * * const toDate = SchemaGetter.Date() * // Getter * ``` * * See also: * - {@link dateTimeUtcFromInput} — validated DateTime parsing * * @category Coercions * @since 4.0.0 */ export function Date(): Getter { return transform((u) => new globalThis.Date(u)) } /** * Trims whitespace from both ends of a string. * * Behavior: * - Pure, delegates to `String.trim`. * * **Example** (Trim whitespace) * * ```ts * import { SchemaGetter } from "effect" * * const trimmed = SchemaGetter.trim() * ``` * * @category string * @since 4.0.0 */ export function trim(): Getter { return transform(Str.trim) } /** * Capitalizes the first character of a string. * * Behavior: * - Pure, delegates to `String.capitalize`. * * **Example** (Capitalize string) * * ```ts * import { SchemaGetter } from "effect" * * const cap = SchemaGetter.capitalize() * ``` * * @category string * @since 4.0.0 */ export function capitalize(): Getter { return transform(Str.capitalize) } /** * Lowercases the first character of a string. * * Behavior: * - Pure, delegates to `String.uncapitalize`. * * **Example** (Uncapitalize string) * * ```ts * import { SchemaGetter } from "effect" * * const uncap = SchemaGetter.uncapitalize() * ``` * * @category string * @since 4.0.0 */ export function uncapitalize(): Getter { return transform(Str.uncapitalize) } /** * Converts a `snake_case` string to `camelCase`. * * Behavior: * - Pure, delegates to `String.snakeToCamel`. * * **Example** (Snake to camel) * * ```ts * import { SchemaGetter } from "effect" * * const toCamel = SchemaGetter.snakeToCamel() * ``` * * See also: * - {@link camelToSnake} — inverse operation * * @category string * @since 4.0.0 */ export function snakeToCamel(): Getter { return transform(Str.snakeToCamel) } /** * Converts a `camelCase` string to `snake_case`. * * Behavior: * - Pure, delegates to `String.camelToSnake`. * * **Example** (Camel to snake) * * ```ts * import { SchemaGetter } from "effect" * * const toSnake = SchemaGetter.camelToSnake() * ``` * * See also: * - {@link snakeToCamel} — inverse operation * * @category string * @since 4.0.0 */ export function camelToSnake(): Getter { return transform(Str.camelToSnake) } /** * Converts a string to lowercase. * * Behavior: * - Pure, delegates to `String.toLowerCase`. * * **Example** (To lowercase) * * ```ts * import { SchemaGetter } from "effect" * * const lower = SchemaGetter.toLowerCase() * ``` * * See also: * - {@link toUpperCase} — inverse operation * * @category string * @since 4.0.0 */ export function toLowerCase(): Getter { return transform(Str.toLowerCase) } /** * Converts a string to uppercase. * * Behavior: * - Pure, delegates to `String.toUpperCase`. * * **Example** (To uppercase) * * ```ts * import { SchemaGetter } from "effect" * * const upper = SchemaGetter.toUpperCase() * ``` * * See also: * - {@link toLowerCase} — inverse operation * * @category string * @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. * * Use this when: * - An encoded value is a JSON string that needs to be parsed during decoding. * * Behavior: * - Skips `None` inputs. * - Without `reviver`: returns `Schema.MutableJson` (typed JSON). * - With `reviver`: returns `unknown` (reviver may produce arbitrary values). * - On parse failure, fails with `Issue.InvalidValue` containing the error message. * * **Example** (Parse JSON) * * ```ts * import { SchemaGetter } from "effect" * * const parse = SchemaGetter.parseJson() * // Getter * ``` * * See also: * - {@link stringifyJson} — inverse operation * * @category Json * @since 4.0.0 */ export function parseJson(): Getter export function parseJson(options: ParseJsonOptions): Getter export function parseJson(options?: ParseJsonOptions | undefined): Getter { return onSome((input) => Effect.try({ try: () => Option.some(JSON.parse(input, options?.reviver)), catch: (e) => new Issue.InvalidValue(Option.some(input), { message: globalThis.String(e) }) }) ) } type StringifyJsonOptions = { readonly replacer?: Parameters[1] readonly space?: Parameters[2] } /** * Stringifies a value to JSON. * * Use this when: * - A decoded value needs to be serialized to a JSON string during encoding. * * Behavior: * - Skips `None` inputs. * - On stringify failure (e.g. circular references), fails with `Issue.InvalidValue`. * - Supports optional `replacer` and `space` options (same as `JSON.stringify`). * * **Example** (Stringify JSON) * * ```ts * import { SchemaGetter } from "effect" * * const stringify = SchemaGetter.stringifyJson() * // Getter * ``` * * See also: * - {@link parseJson} — inverse operation * * @category Json * @since 4.0.0 */ export function stringifyJson(options?: StringifyJsonOptions): Getter { return onSome((input) => Effect.try({ try: () => Option.some(JSON.stringify(input, options?.replacer, options?.space)), catch: (e) => new Issue.InvalidValue(Option.some(input), { message: globalThis.String(e) }) }) ) } /** * Parses a string into a record of key-value pairs. * * Use this when: * - An encoded string contains delimited key-value pairs (e.g. `"a=1,b=2"`). * * Behavior: * - Splits the string by `separator` (default `,`), then each pair by `keyValueSeparator` (default `=`). * - Pairs missing a key or value are silently skipped. * - Pure, never fails. * * **Example** (Parse key-value string) * * ```ts * import { SchemaGetter } from "effect" * * const parse = SchemaGetter.splitKeyValue() * // "a=1,b=2" -> { a: "1", b: "2" } * ``` * * See also: * - {@link joinKeyValue} — inverse operation * - {@link split} — split into an array of strings * * @category string * @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) => { const [key, value] = pair.split(keyValueSeparator) if (key && value) { acc[key] = value } return acc }, {} as Record) ) } /** * Joins a record of key-value pairs into a delimited string. * * Use this when: * - A decoded record needs to be serialized as a delimited key-value string. * * Behavior: * - Joins entries with `separator` (default `,`) and key/value with `keyValueSeparator` (default `=`). * - Pure, never fails. * * **Example** (Join key-value record) * * ```ts * import { SchemaGetter } from "effect" * * const join = SchemaGetter.joinKeyValue() * // { a: "1", b: "2" } -> "a=1,b=2" * ``` * * See also: * - {@link splitKeyValue} — inverse operation * * @category string * @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. * * Use this when: * - An encoded string is a delimited list (e.g. CSV values). * * Behavior: * - Splits by `separator` (default `,`). * - An empty string produces an empty array (not `[""]`). * - Pure, never fails. * * **Example** (Split comma-separated string) * * ```ts * import { SchemaGetter } from "effect" * * const splitComma = SchemaGetter.split() * // "a,b,c" -> ["a", "b", "c"] * // "" -> [] * ``` * * See also: * - {@link splitKeyValue} — when values are key-value pairs * * @category string * @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. * * Behavior: * - Pure, never fails. * * **Example** (Encode to Base64) * * ```ts * import { SchemaGetter } from "effect" * * const encode = SchemaGetter.encodeBase64() * ``` * * See also: * - {@link decodeBase64} — inverse (to `Uint8Array`) * - {@link decodeBase64String} — inverse (to `string`) * - {@link encodeBase64Url} — URL-safe variant * * @category Base64 * @since 4.0.0 */ export function encodeBase64(): Getter { return transform(Encoding.encodeBase64) } /** * Encodes a `Uint8Array` or string to a URL-safe Base64 string. * * Behavior: * - Pure, never fails. * * **Example** (Encode to Base64Url) * * ```ts * import { SchemaGetter } from "effect" * * const encode = SchemaGetter.encodeBase64Url() * ``` * * See also: * - {@link decodeBase64Url} — inverse (to `Uint8Array`) * - {@link decodeBase64UrlString} — inverse (to `string`) * - {@link encodeBase64} — standard Base64 variant * * @category Base64 * @since 4.0.0 */ export function encodeBase64Url(): Getter { return transform(Encoding.encodeBase64Url) } /** * Encodes a `Uint8Array` or string to a hexadecimal string. * * Behavior: * - Pure, never fails. * * **Example** (Encode to hex) * * ```ts * import { SchemaGetter } from "effect" * * const encode = SchemaGetter.encodeHex() * ``` * * See also: * - {@link decodeHex} — inverse (to `Uint8Array`) * - {@link decodeHexString} — inverse (to `string`) * * @category Hex * @since 4.0.0 */ export function encodeHex(): Getter { return transform(Encoding.encodeHex) } /** * Decodes a Base64 string to a `Uint8Array`. * * Behavior: * - Fails with `Issue.InvalidValue` if the input is not valid Base64. * * **Example** (Decode Base64 to bytes) * * ```ts * import { SchemaGetter } from "effect" * * const decode = SchemaGetter.decodeBase64() * // Getter * ``` * * See also: * - {@link decodeBase64String} — decode to `string` instead * - {@link encodeBase64} — inverse operation * * @category Base64 * @since 4.0.0 */ export function decodeBase64(): Getter { return transformOrFail((input) => Effect.mapErrorEager( Effect.fromResult(Encoding.decodeBase64(input)), (e) => new Issue.InvalidValue(Option.some(input), { message: e.message }) ) ) } /** * Decodes a Base64 string to a UTF-8 `string`. * * Behavior: * - Fails with `Issue.InvalidValue` if the input is not valid Base64. * * **Example** (Decode Base64 to string) * * ```ts * import { SchemaGetter } from "effect" * * const decode = SchemaGetter.decodeBase64String() * // Getter * ``` * * See also: * - {@link decodeBase64} — decode to `Uint8Array` instead * - {@link encodeBase64} — inverse operation * * @category Base64 * @since 4.0.0 */ export function decodeBase64String(): Getter { return transformOrFail((input) => Result.match(Encoding.decodeBase64String(input), { onFailure: (e) => Effect.fail(new Issue.InvalidValue(Option.some(input), { message: e.message })), onSuccess: Effect.succeed }) ) } /** * Decodes a URL-safe Base64 string to a `Uint8Array`. * * Behavior: * - Fails with `Issue.InvalidValue` if the input is not valid Base64Url. * * **Example** (Decode Base64Url to bytes) * * ```ts * import { SchemaGetter } from "effect" * * const decode = SchemaGetter.decodeBase64Url() * // Getter * ``` * * See also: * - {@link decodeBase64UrlString} — decode to `string` instead * - {@link encodeBase64Url} — inverse operation * * @category Base64 * @since 4.0.0 */ export function decodeBase64Url(): Getter { return transformOrFail((input) => Result.match(Encoding.decodeBase64Url(input), { onFailure: (e) => Effect.fail(new Issue.InvalidValue(Option.some(input), { message: e.message })), onSuccess: Effect.succeed }) ) } /** * Decodes a URL-safe Base64 string to a UTF-8 `string`. * * Behavior: * - Fails with `Issue.InvalidValue` if the input is not valid Base64Url. * * **Example** (Decode Base64Url to string) * * ```ts * import { SchemaGetter } from "effect" * * const decode = SchemaGetter.decodeBase64UrlString() * // Getter * ``` * * See also: * - {@link decodeBase64Url} — decode to `Uint8Array` instead * - {@link encodeBase64Url} — inverse operation * * @category Base64 * @since 4.0.0 */ export function decodeBase64UrlString(): Getter { return transformOrFail((input) => Result.match(Encoding.decodeBase64UrlString(input), { onFailure: (e) => Effect.fail(new Issue.InvalidValue(Option.some(input), { message: e.message })), onSuccess: Effect.succeed }) ) } /** * Decodes a hexadecimal string to a `Uint8Array`. * * Behavior: * - Fails with `Issue.InvalidValue` if the input is not valid hex. * * **Example** (Decode hex to bytes) * * ```ts * import { SchemaGetter } from "effect" * * const decode = SchemaGetter.decodeHex() * // Getter * ``` * * See also: * - {@link decodeHexString} — decode to `string` instead * - {@link encodeHex} — inverse operation * * @category Hex * @since 4.0.0 */ export function decodeHex(): Getter { return transformOrFail((input) => Result.match(Encoding.decodeHex(input), { onFailure: (e) => Effect.fail(new Issue.InvalidValue(Option.some(input), { message: e.message })), onSuccess: Effect.succeed }) ) } /** * Decodes a hexadecimal string to a UTF-8 `string`. * * Behavior: * - Fails with `Issue.InvalidValue` if the input is not valid hex. * * **Example** (Decode hex to string) * * ```ts * import { SchemaGetter } from "effect" * * const decode = SchemaGetter.decodeHexString() * // Getter * ``` * * See also: * - {@link decodeHex} — decode to `Uint8Array` instead * - {@link encodeHex} — inverse operation * * @category Hex * @since 4.0.0 */ export function decodeHexString(): Getter { return transformOrFail((input) => Result.match(Encoding.decodeHexString(input), { onFailure: (e) => Effect.fail(new Issue.InvalidValue(Option.some(input), { message: e.message })), onSuccess: Effect.succeed }) ) } /** * Encodes a string using `encodeURIComponent`. * * Behavior: * - Pure, never fails. * * **Example** (Encode a URI component) * * ```ts * import { SchemaGetter } from "effect" * * const encode = SchemaGetter.encodeUriComponent() * ``` * * See also: * - {@link decodeUriComponent} - inverse operation * * @category URI * @since 4.0.0 */ export function encodeUriComponent(): Getter { return transform(encodeURIComponent) } /** * Decodes a URI component encoded string using `decodeURIComponent`. * * Behavior: * - Fails with `Issue.InvalidValue` if the input contains malformed percent-encoding sequences. * * **Example** (Decode a URI component) * * ```ts * import { SchemaGetter } from "effect" * * const decode = SchemaGetter.decodeUriComponent() * // Getter * ``` * * See also: * - {@link encodeUriComponent} - inverse operation * * @category URI * @since 4.0.0 */ export function decodeUriComponent(): Getter { return transformOrFail((input) => { try { return Effect.succeed(globalThis.decodeURIComponent(input)) } catch (e) { return Effect.fail( new Issue.InvalidValue(Option.some(input), { message: e instanceof URIError ? e.message : "Invalid URI component" }) ) } }) } /** * Parses a `DateTime.Input` value (string, number, or Date) into a `DateTime.Utc`. * * Use this when: * - An encoded value represents a date/time and should be decoded to a `DateTime.Utc`. * * Behavior: * - Fails with `Issue.InvalidValue` if the input cannot be parsed as a valid DateTime. * * **Example** (Parse DateTime) * * ```ts * import { SchemaGetter } from "effect" * * const parseDate = SchemaGetter.dateTimeUtcFromInput() * // Getter * ``` * * See also: * - {@link Date} — simpler coercion to `Date` (no validation) * * @category DateTime * @since 4.0.0 */ export function dateTimeUtcFromInput(): Getter { return transformOrFail((input) => { return Option.match(DateTime.make(input), { onNone: () => Effect.fail(new Issue.InvalidValue(Option.some(input), { message: "Invalid DateTime input" })), onSome: (dt) => Effect.succeed(DateTime.toUtc(dt)) }) }) } /** * Decodes a `FormData` object into a nested tree structure using bracket-path notation. * * Use this when: * - Parsing `FormData` from HTTP requests into structured objects. * * Behavior: * - Pure, never fails. * - Interprets bracket-path keys (e.g. `user[name]`, `items[0]`) to build nested objects/arrays. * - Leaf values are `string` or `Blob`. * * **Example** (Decode FormData) * * ```ts * import { SchemaGetter } from "effect" * * const decode = SchemaGetter.decodeFormData() * // Getter, FormData> * ``` * * See also: * - {@link encodeFormData} — inverse operation * - {@link makeTreeRecord} — the underlying bracket-path parser * - {@link decodeURLSearchParams} — similar for URLSearchParams * * @category FormData * @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. * * Use this when: * - Serializing structured data to `FormData` for HTTP requests. * * Behavior: * - Pure, never fails. * - Flattens nested objects/arrays into bracket-path keys (e.g. `user[name]`, `items[0]`). * - Non-object inputs produce an empty `FormData`. * * **Example** (Encode to FormData) * * ```ts * import { SchemaGetter } from "effect" * * const encode = SchemaGetter.encodeFormData() * // Getter * ``` * * See also: * - {@link decodeFormData} — inverse operation * - {@link collectBracketPathEntries} — the underlying flattener * - {@link encodeURLSearchParams} — similar for URLSearchParams * * @category FormData * @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. * * Use this when: * - Parsing query parameters from URLs into structured objects. * * Behavior: * - Pure, never fails. * - Interprets bracket-path keys (e.g. `user[name]`, `items[0]`) to build nested objects/arrays. * - Leaf values are `string`. * * **Example** (Decode URLSearchParams) * * ```ts * import { SchemaGetter } from "effect" * * const decode = SchemaGetter.decodeURLSearchParams() * // Getter, URLSearchParams> * ``` * * See also: * - {@link encodeURLSearchParams} — inverse operation * - {@link makeTreeRecord} — the underlying bracket-path parser * - {@link decodeFormData} — similar for FormData * * @category URLSearchParams * @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. * * Use this when: * - Serializing structured data to query parameters for URLs. * * Behavior: * - Pure, never fails. * - Flattens nested objects/arrays into bracket-path keys. * - Non-object inputs produce an empty `URLSearchParams`. * * **Example** (Encode to URLSearchParams) * * ```ts * import { SchemaGetter } from "effect" * * const encode = SchemaGetter.encodeURLSearchParams() * // Getter * ``` * * See also: * - {@link decodeURLSearchParams} — inverse operation * - {@link collectBracketPathEntries} — the underlying flattener * - {@link encodeFormData} — similar for FormData * * @category URLSearchParams * @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() }) } const INDEX_REGEXP = /^\d+$/ 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) => (INDEX_REGEXP.test(part) ? globalThis.Number(part) : part)) } /** * Builds a nested tree object from a list of bracket-path entries. * * A bracket path is a string like `"user[address][city]"` that describes nested * object/array structure. This function interprets those paths and constructs the * corresponding nested object. * * Use this when: * - Parsing FormData or URLSearchParams entries into structured objects. * - You have flat key-value pairs with bracket-path keys that need nesting. * * Behavior: * - Mutates and returns a new object (does not mutate the input array). * - 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 * - Duplicate keys for the same path are merged into arrays. * * **Example** (Build tree from bracket paths) * * ```ts * import { SchemaGetter } from "effect" * * const tree = SchemaGetter.makeTreeRecord([ * ["user[name]", "Alice"], * ["user[tags][]", "admin"], * ["user[tags][]", "editor"] * ]) * // { user: { name: "Alice", tags: ["admin", "editor"] } } * ``` * * See also: * - {@link collectBracketPathEntries} — inverse operation (tree to flat entries) * - {@link decodeFormData} — uses this internally * - {@link decodeURLSearchParams} — uses this internally * * @category Tree * @since 4.0.0 */ export function makeTreeRecord( bracketPathEntries: ReadonlyArray ): Schema.TreeRecord { const out: any = {} bracketPathEntries.forEach(([key, value]) => { const tokens = bracketPathToTokens(key) let cur: any = out tokens.forEach((token, i) => { const isLast = i === tokens.length - 1 // We are inside an array and see "[]" (empty token) => append if (Array.isArray(cur) && token === "") { if (isLast) { cur.push(value) } else { // bracket path: "foo[][bar]" => push a new element and descend into it const next = tokens[i + 1] const shouldBeArray = typeof next === "number" || next === "" const index = cur.length if (cur[index] === undefined) { cur[index] = shouldBeArray ? [] : {} } cur = cur[index] } } else if (isLast) { // If we're setting a value at a path that already exists // convert it to an array to support multiple values for the same key if (Array.isArray(cur[token])) { cur[token].push(value) } else if (Object.prototype.hasOwnProperty.call(cur, token)) { cur[token] = [cur[token], value] } else { cur[token] = value } } else { const next = tokens[i + 1] // if next is a number OR "" (from []), we are building an array const shouldBeArray = typeof next === "number" || next === "" if (cur[token] === undefined) { cur[token] = shouldBeArray ? [] : {} } cur = cur[token] } }) }) return out } /** * Flattens a nested object into bracket-path entries, filtering leaf values by a type guard. * * This is the inverse of {@link makeTreeRecord}. It takes a nested object and produces * flat `[bracketPath, value]` pairs suitable for `FormData` or `URLSearchParams`. * * Use this when: * - Serializing structured objects to flat key-value entries. * - Building custom `FormData` or `URLSearchParams` encoders. * * Behavior: * - 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. * * **Example** (Flatten object to bracket paths) * * ```ts * import { SchemaGetter, Predicate } from "effect" * * const collectStrings = SchemaGetter.collectBracketPathEntries(Predicate.isString) * const entries = collectStrings({ user: { name: "Alice", tags: ["admin", "editor"] } }) * // [["user[name]", "Alice"], ["user[tags]", "admin"], ["user[tags]", "editor"]] * ``` * * See also: * - {@link makeTreeRecord} — inverse operation (flat entries to tree) * - {@link encodeFormData} — uses this internally * - {@link encodeURLSearchParams} — uses this internally * * @category Tree * @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 } }