1/**
2 * Builds one-way conversions used by schemas.
3 *
4 * A `Getter<T, E, R>` receives an optional encoded value and returns an
5 * optional decoded value. It can also report a schema issue or require Effect
6 * services. Schema transformations use getters to describe one direction of a
7 * conversion, for example decoding a field from input data. This module
8 * includes basic getters, validation helpers, pure and effectful conversions,
9 * and ready-made conversions for common string, number, binary, date, form, and
10 * URL-related values.
11 *
12 * @since 4.0.0
13 */
14import * as Arr from "./Array.ts"
15import * as DateTime from "./DateTime.ts"
16import * as Effect from "./Effect.ts"
17import * as Base64 from "./encoding/Base64.ts"
18import * as Base64Url from "./encoding/Base64Url.ts"
19import * as Hex from "./encoding/Hex.ts"
20import { dual } from "./Function.ts"
21import * as InternalRecord from "./internal/record.ts"
22import * as Option from "./Option.ts"
23import * as Pipeable from "./Pipeable.ts"
24import * as Predicate from "./Predicate.ts"
25import * as Result from "./Result.ts"
26import type * as Schema from "./Schema.ts"
27import type * as SchemaAST from "./SchemaAST.ts"
28import * as SchemaIssue from "./SchemaIssue.ts"
29import * as Str from "./String.ts"
30
31/**
32 * A transformation that returns its input unchanged.
33 *
34 * @category models
35 * @since 4.0.0
36 */
37export interface Passthrough extends Pipeable.Pipeable {
38 readonly _tag: "Passthrough"
39}
40
41/**
42 * A synchronous transformation of present values.
43 *
44 * @category models
45 * @since 4.0.0
46 */
47export interface Transform<out T, in E> extends Pipeable.Pipeable {
48 readonly _tag: "Transform"
49 readonly transform: (input: E) => T
50}
51
52/**
53 * A synchronous transformation of optional values.
54 *
55 * @category models
56 * @since 4.0.0
57 */
58export interface TransformOptional<out T, in E> extends Pipeable.Pipeable {
59 readonly _tag: "TransformOptional"
60 readonly transform: (input: Option.Option<E>) => Option.Option<T>
61}
62
63/**
64 * An effectful transformation of present values.
65 *
66 * @category models
67 * @since 4.0.0
68 */
69export interface TransformEffect<out T, in E, R> extends Pipeable.Pipeable {
70 readonly _tag: "TransformEffect"
71 readonly transform: (
72 input: E,
73 options: SchemaAST.ParseOptions
74 ) => Effect.Effect<T, SchemaIssue.Issue, R>
75}
76
77/**
78 * An effectful transformation of optional values.
79 *
80 * @category models
81 * @since 4.0.0
82 */
83export interface TransformOptionalEffect<out T, in E, R> extends Pipeable.Pipeable {
84 readonly _tag: "TransformOptionalEffect"
85 readonly transform: (
86 input: Option.Option<E>,
87 options: SchemaAST.ParseOptions
88 ) => Effect.Effect<Option.Option<T>, SchemaIssue.Issue, R>
89}
90
91/**
92 * Represents a composable transformation from an encoded type `E` to a decoded type `T`.
93 *
94 * **When to use**
95 *
96 * Use when you need a schema getter to build and compose custom transformations
97 * for `Schema.decodeTo` or `Schema.decode`.
98 *
99 * **Details**
100 *
101 * A getter receives an `Option<E>` and produces an `Option<T>`. `Option.none()`
102 * represents a missing struct field and can also omit a field from the output.
103 * A getter may fail with a schema issue or require services through `R`. The
104 * tagged representation distinguishes synchronous transformations from
105 * transformations that return an `Effect`, allowing schema parsers to select
106 * the corresponding execution path when they are built. Getter values expose
107 * `pipe`; use the standalone {@link map}, {@link compose}, and {@link run}
108 * functions to operate on them.
109 *
110 * **Example** (Creating and composing getters)
111 *
112 * ```ts import.meta.vitest
113 * import { Effect, Option, SchemaGetter } from "effect"
114 *
115 * const parseNumber = SchemaGetter.transform<number, string>((s) => Number(s))
116 * const double = SchemaGetter.transform<number, number>((n) => n * 2)
117 * const composed = SchemaGetter.compose(parseNumber, double)
118 * Effect.runSync(SchemaGetter.run(composed, Option.some("21"), {})) // => Option.some(42)
119 * ```
120 *
121 * @see {@link passthrough} for the identity getter
122 * @see {@link transform} to create a getter from a pure function
123 * @see {@link transformEffect} for effectful transformation
124 *
125 * @category models
126 * @since 4.0.0
127 */
128export type Getter<T, E, R = never> =
129 | Passthrough
130 | Transform<T, E>
131 | TransformOptional<T, E>
132 | TransformEffect<T, E, R>
133 | TransformOptionalEffect<T, E, R>
134
135const runGetter = <T, E, R>(
136 self: Getter<T, E, R>,
137 input: Option.Option<E>,
138 options: SchemaAST.ParseOptions
139): Effect.Effect<Option.Option<T>, SchemaIssue.Issue, R> => {
140 switch (self._tag) {
141 case "Passthrough":
142 return Effect.succeed(input as unknown as Option.Option<T>)
143 case "Transform":
144 return Effect.succeed(Option.map(input, self.transform))
145 case "TransformOptional":
146 return Effect.succeed(self.transform(input))
147 case "TransformEffect":
148 return Option.isNone(input)
149 ? Effect.succeedNone
150 : Effect.mapEager(self.transform(input.value, options), Option.some)
151 case "TransformOptionalEffect":
152 return self.transform(input, options)
153 }
154}
155
156/**
157 * Runs a getter directly.
158 *
159 * **Details**
160 *
161 * This is a convenience API for executing a getter outside a schema. When the
162 * getter belongs to a schema, use the corresponding `SchemaParser` API. The
163 * result is an `Effect` for every getter variant, including synchronous ones.
164 *
165 * **Example** (Running a getter)
166 *
167 * ```ts import.meta.vitest
168 * import { Effect, Option, SchemaGetter } from "effect"
169 *
170 * const getter = SchemaGetter.transform<number, string>(Number)
171 *
172 * const result = Effect.runSync(
173 * SchemaGetter.run(getter, Option.some("42"), {})
174 * )
175 * result // => Option.some(42)
176 * ```
177 *
178 * @category converting
179 * @since 4.0.0
180 */
181export const run: {
182 <E>(
183 input: Option.Option<E>,
184 options: SchemaAST.ParseOptions
185 ): <T, R>(self: Getter<T, E, R>) => Effect.Effect<Option.Option<T>, SchemaIssue.Issue, R>
186 <T, E, R>(
187 self: Getter<T, E, R>,
188 input: Option.Option<E>,
189 options: SchemaAST.ParseOptions
190 ): Effect.Effect<Option.Option<T>, SchemaIssue.Issue, R>
191} = dual(3, runGetter)
192
193const makeGetter = <const A extends object>(fields: A): A & Pipeable.Pipeable =>
194 Object.assign(Object.create(Pipeable.Prototype), fields)
195
196const composeOptionalEffect = <T, E, R, T2, R2>(
197 first: Getter<T, E, R>,
198 second: Getter<T2, T, R2>
199): Getter<T2, E, R | R2> =>
200 transformOptionalEffect((input, options) =>
201 Effect.flatMapEager(runGetter(first, input, options), (output) => runGetter(second, output, options))
202 )
203
204/**
205 * Composes two getters by passing the output of the first to the second.
206 *
207 * **When to use**
208 *
209 * Use when a schema conversion requires multiple transformation steps.
210 *
211 * **Details**
212 *
213 * Composition forwards `Option.none()` to getters that handle optional values
214 * and skips getters that operate only on present values. Composing with
215 * {@link passthrough} returns the other getter unchanged. The function supports
216 * both `compose(first, second)` and `first.pipe(compose(second))`.
217 *
218 * **Example** (Parsing and normalizing a number)
219 *
220 * ```ts import.meta.vitest
221 * import { Effect, Option, SchemaGetter } from "effect"
222 *
223 * const getter = SchemaGetter.compose(
224 * SchemaGetter.transform<number, string>(Number),
225 * SchemaGetter.transform((n) => Math.max(0, n))
226 * )
227 *
228 * Effect.runSync(SchemaGetter.run(getter, Option.some("-1"), {})) // => Option.some(0)
229 * ```
230 *
231 * @category combining
232 * @since 4.0.0
233 */
234export const compose: {
235 <T, T2, R2>(other: Getter<T2, T, R2>): <E, R>(self: Getter<T, E, R>) => Getter<T2, E, R | R2>
236 <T, E, R, T2, R2>(self: Getter<T, E, R>, other: Getter<T2, T, R2>): Getter<T2, E, R | R2>
237} = dual(2, <T, E, R, T2, R2>(
238 self: Getter<T, E, R>,
239 other: Getter<T2, T, R2>
240): Getter<T2, E, R | R2> => {
241 if (self._tag === "Passthrough") return other as Getter<T2, E, R | R2>
242 if (other._tag === "Passthrough") return self as unknown as Getter<T2, E, R | R2>
243 switch (self._tag) {
244 case "Transform": {
245 switch (other._tag) {
246 case "Transform":
247 return transform((input: E) => other.transform(self.transform(input)))
248 case "TransformOptional":
249 return transformOptional((input) => other.transform(Option.map(input, self.transform)))
250 case "TransformEffect":
251 return transformEffect((input: E, options) => other.transform(self.transform(input), options))
252 case "TransformOptionalEffect":
253 return composeOptionalEffect(self, other)
254 }
255 }
256 case "TransformOptional": {
257 switch (other._tag) {
258 case "Transform":
259 return transformOptional((input) => Option.map(self.transform(input), other.transform))
260 case "TransformOptional":
261 return transformOptional((input) => other.transform(self.transform(input)))
262 case "TransformEffect":
263 case "TransformOptionalEffect":
264 return composeOptionalEffect(self, other)
265 }
266 }
267 case "TransformEffect": {
268 switch (other._tag) {
269 case "Transform":
270 return transformEffect((input: E, options) =>
271 Effect.mapEager(self.transform(input, options), other.transform)
272 )
273 case "TransformOptional":
274 case "TransformOptionalEffect":
275 return composeOptionalEffect(self, other)
276 case "TransformEffect":
277 return transformEffect((input: E, options) =>
278 Effect.flatMapEager(self.transform(input, options), (output) => other.transform(output, options))
279 )
280 }
281 }
282 case "TransformOptionalEffect":
283 return composeOptionalEffect(self, other)
284 }
285})
286
287/**
288 * Maps the output of a getter while preserving missing values.
289 *
290 * **When to use**
291 *
292 * Use to add a synchronous transformation after an existing getter.
293 *
294 * **Details**
295 *
296 * The mapping function runs only for `Option.some`. The function supports both
297 * `map(self, f)` and `self.pipe(map(f))`.
298 *
299 * **Example** (Mapping a getter result)
300 *
301 * ```ts import.meta.vitest
302 * import { Effect, Option, SchemaGetter } from "effect"
303 *
304 * const getter = SchemaGetter.transform<number, string>(Number).pipe(
305 * SchemaGetter.map((n) => n * 2)
306 * )
307 *
308 * Effect.runSync(SchemaGetter.run(getter, Option.some("21"), {})) // => Option.some(42)
309 * ```
310 *
311 * @category mapping
312 * @since 4.0.0
313 */
314export const map: {
315 <T, T2>(f: (value: T) => T2): <E, R>(self: Getter<T, E, R>) => Getter<T2, E, R>
316 <T, E, R, T2>(self: Getter<T, E, R>, f: (value: T) => T2): Getter<T2, E, R>
317} = dual(2, <T, E, R, T2>(self: Getter<T, E, R>, f: (value: T) => T2): Getter<T2, E, R> => compose(self, transform(f)))
318
319/**
320 * Creates a getter that always produces the given constant value, ignoring the input.
321 *
322 * **When to use**
323 *
324 * Use when you need a schema getter that always decodes a field to a fixed
325 * value.
326 *
327 * **Details**
328 *
329 * The getter is pure and always returns `Option.some(t)` regardless of whether
330 * the input is `Some` or `None`.
331 *
332 * **Example** (Returning a constant getter)
333 *
334 * ```ts import.meta.vitest
335 * import { Effect, Option, SchemaGetter } from "effect"
336 *
337 * const alwaysZero = SchemaGetter.succeed(0)
338 * Effect.runSync(SchemaGetter.run(alwaysZero, Option.none(), {})) // => Option.some(0)
339 * ```
340 *
341 * @see {@link transform} when you need to use the input value
342 * @see {@link passthrough} when you want to keep the input as-is
343 *
344 * @category constructors
345 * @since 4.0.0
346 */
347export function succeed<const T, E>(t: T): Getter<T, E> {
348 return transformOptional(() => Option.some(t))
349}
350
351/**
352 * Creates a getter that always fails with the given issue.
353 *
354 * **When to use**
355 *
356 * Use when you need a schema getter that unconditionally rejects input.
357 * - Building custom validation getters that produce specific error types.
358 *
359 * **Details**
360 *
361 * - Always fails with the `Issue` returned by `f`.
362 * - The failure function receives the original `Option<E>` input and the
363 * effective `ParseOptions` for error context.
364 *
365 * **Example** (Defining an always-failing getter)
366 *
367 * ```ts import.meta.vitest
368 * import { Effect, Option, SchemaGetter, SchemaIssue } from "effect"
369 *
370 * const rejectAll = SchemaGetter.fail<string, string>(
371 * () => new SchemaIssue.InvalidValue({ message: "not allowed" })
372 * )
373 * const issue = await Effect.runPromise(
374 * Effect.flip(SchemaGetter.run(rejectAll, Option.some("x"), {}))
375 * )
376 * issue._tag // => "InvalidValue"
377 * ```
378 *
379 * @see {@link forbidden} for a convenience helper for `Forbidden` issues
380 * @see {@link checkEffect} to fail conditionally based on input value
381 *
382 * @category constructors
383 * @since 4.0.0
384 */
385export function fail<T, E>(
386 f: (oe: Option.Option<E>, options: SchemaAST.ParseOptions) => SchemaIssue.Issue
387): Getter<T, E> {
388 return transformOptionalEffect((oe, options) => Effect.fail(f(oe, options)))
389}
390
391/**
392 * Creates a getter that always fails with a `Forbidden` issue.
393 *
394 * **When to use**
395 *
396 * Use when you need a schema getter to disallow a field or direction
397 * (encode/decode) entirely.
398 * - You want a clear "forbidden" error message in schema validation output.
399 *
400 * **Details**
401 *
402 * - Always fails with `SchemaIssue.Forbidden`.
403 * - The message function receives the `Option<E>` input for context.
404 *
405 * **Example** (Forbidding a decode direction)
406 *
407 * ```ts import.meta.vitest
408 * import { Effect, Option, SchemaGetter } from "effect"
409 *
410 * const noEncode = SchemaGetter.forbidden<string, number>(
411 * () => "encoding is not supported"
412 * )
413 * const issue = await Effect.runPromise(
414 * Effect.flip(SchemaGetter.run(noEncode, Option.some(1), {}))
415 * )
416 * issue._tag // => "Forbidden"
417 * ```
418 *
419 * @see {@link fail} to fail with a custom issue type
420 *
421 * @category constructors
422 * @since 4.0.0
423 */
424export function forbidden<T, E>(message: (oe: Option.Option<E>) => string): Getter<T, E> {
425 return fail<T, E>((oe, options) => {
426 const annotations = { message: message(oe) }
427 return Option.isSome(oe)
428 ? new SchemaIssue.Forbidden(annotations, oe.value, options)
429 : new SchemaIssue.Forbidden(annotations)
430 })
431}
432
433/**
434 * Getter that always fails with a `Forbidden` issue indicating that encoding is unsupported.
435 *
436 * **When to use**
437 *
438 * Use as the encode side of a decode-only Schema transformation.
439 *
440 * **Details**
441 *
442 * Its `Getter<never, unknown>` type is assignable to every encoding getter because it accepts any input and never
443 * produces an output value.
444 *
445 * **Example** (Rejecting encoding)
446 *
447 * ```ts import.meta.vitest
448 * import { Effect, Option, SchemaGetter } from "effect"
449 *
450 * const issue = await Effect.runPromise(
451 * Effect.flip(SchemaGetter.run(SchemaGetter.forbiddenEncoding, Option.some("value"), {}))
452 * )
453 * issue._tag // => "Forbidden"
454 * ```
455 *
456 * @see {@link forbidden} for a forbidden getter with a custom message
457 *
458 * @category constructors
459 * @since 4.0.0
460 */
461export const forbiddenEncoding: Getter<never, unknown> = forbidden(() => "Encoding is not supported")
462
463const passthrough_: Passthrough = makeGetter({ _tag: "Passthrough" })
464
465/**
466 * Returns the identity getter — passes the value through unchanged.
467 *
468 * **When to use**
469 *
470 * Use when you need a schema getter for one side of a `decodeTo` pair, either
471 * encode or decode, to pass values through unchanged.
472 *
473 * **Details**
474 *
475 * - Pure, no allocation (singleton instance).
476 * - Optimized away by {@link compose} — composing with a passthrough is free.
477 * - The default overload requires `T === E`. Pass `{ strict: false }` to opt
478 * out of the type constraint.
479 *
480 * **Example** (Passing through identity transformations)
481 *
482 * ```ts import.meta.vitest
483 * import { Schema, SchemaGetter } from "effect"
484 *
485 * // No transformation needed — types already match
486 * const StringToString = Schema.String.pipe(
487 * Schema.decodeTo(Schema.String, {
488 * decode: SchemaGetter.passthrough(),
489 * encode: SchemaGetter.passthrough()
490 * })
491 * )
492 * Schema.decodeSync(StringToString)("hello") // => "hello"
493 * ```
494 *
495 * @see {@link passthroughSupertype} when `T extends E`
496 * @see {@link passthroughSubtype} when `E extends T`
497 * @see {@link transform} when you need to change the value
498 *
499 * @category constructors
500 * @since 4.0.0
501 */
502export function passthrough<T, E>(options: { readonly strict: false }): Getter<T, E>
503export function passthrough<T>(): Getter<T, T>
504export function passthrough<T>(): Getter<T, T> {
505 return passthrough_
506}
507
508/**
509 * Returns the identity getter typed for the relationship `T extends E`.
510 *
511 * **When to use**
512 *
513 * Use when you need a schema getter that passes values through when the
514 * decoded/output type is narrower than the encoded/input type.
515 *
516 * **Details**
517 *
518 * - Same singleton as {@link passthrough} — no allocation, optimized in composition.
519 *
520 * **Example** (Passing through supertypes)
521 *
522 * ```ts import.meta.vitest
523 * import { Effect, Option, SchemaGetter } from "effect"
524 *
525 * // string extends string, so this is valid
526 * const g = SchemaGetter.passthroughSupertype<string, string>()
527 * Effect.runSync(SchemaGetter.run(g, Option.some("hello"), {})) // => Option.some("hello")
528 * ```
529 *
530 * @see {@link passthrough} when types are identical
531 * @see {@link passthroughSubtype} when `E extends T`
532 *
533 * @category constructors
534 * @since 4.0.0
535 */
536export function passthroughSupertype<T extends E, E>(): Getter<T, E>
537export function passthroughSupertype<T>(): Getter<T, T> {
538 return passthrough_
539}
540
541/**
542 * Returns the identity getter, typed for when the encoded type `E` is a subtype of `T`.
543 *
544 * **When to use**
545 *
546 * Use when you need a schema getter that passes values through without
547 * `{ strict: false }` for an encoded type that narrows the decoded type.
548 *
549 * **Details**
550 *
551 * - Same singleton as {@link passthrough} — no allocation, optimized in composition.
552 *
553 * **Example** (Passing through subtypes)
554 *
555 * ```ts import.meta.vitest
556 * import { Effect, Option, SchemaGetter } from "effect"
557 *
558 * // "hello" extends string, so E extends T
559 * const g = SchemaGetter.passthroughSubtype<string, "hello">()
560 * Effect.runSync(SchemaGetter.run(g, Option.some("hello"), {})) // => Option.some("hello")
561 * ```
562 *
563 * @see {@link passthrough} when types are identical
564 * @see {@link passthroughSupertype} when `T extends E`
565 *
566 * @category constructors
567 * @since 4.0.0
568 */
569export function passthroughSubtype<T, E extends T>(): Getter<T, E>
570export function passthroughSubtype<T>(): Getter<T, T> {
571 return passthrough_
572}
573
574/**
575 * Creates a getter that fails with `MissingKey` if the input is absent (`Option.None`).
576 *
577 * **When to use**
578 *
579 * Use when you need a schema getter to require a struct field in the encoded
580 * input and report a missing key error when it is absent.
581 *
582 * **Details**
583 *
584 * - When input is `None`, fails with `SchemaIssue.MissingKey`.
585 * - When input is `Some`, passes it through unchanged.
586 * - Optional `annotations` customize the error message for the missing key.
587 *
588 * **Example** (Defining a required struct field)
589 *
590 * ```ts import.meta.vitest
591 * import { Effect, Option, SchemaGetter } from "effect"
592 *
593 * const mustExist = SchemaGetter.required<string>()
594 * const issue = await Effect.runPromise(
595 * Effect.flip(SchemaGetter.run(mustExist, Option.none(), {}))
596 * )
597 * issue._tag // => "MissingKey"
598 * ```
599 *
600 * @see {@link withDefault} to substitute a default for undefined values
601 *
602 * @category validation
603 * @since 4.0.0
604 */
605export function required<T, E extends T = T>(annotations?: Schema.Annotations.Key<T>): Getter<T, E> {
606 return transformOptionalEffect((input) =>
607 Option.isNone(input) ? Effect.fail(new SchemaIssue.MissingKey(annotations)) : Effect.succeed(input)
608 )
609}
610
611/**
612 * Creates a getter that validates a value using an effectful check function.
613 *
614 * **When to use**
615 *
616 * Use when you need a schema getter to validate a decoded value (e.g. check a
617 * constraint or call an external service).
618 * - The validation may be asynchronous or require Effect services.
619 *
620 * **Details**
621 *
622 * - Only runs when input is `Some` — `None` passes through.
623 * - The check function returns a validation result:
624 * - `undefined` or `true` — value is valid, passes through.
625 * - `false` or a `string` — value is invalid, fails with an `Issue`.
626 * - An `Issue` object — fails with that issue directly.
627 * - `{ path, issue }` — fails with a nested path issue (`issue` may be a
628 * message string or a full {@link SchemaIssue.Issue}).
629 * - Does not transform the value — input and output types are the same.
630 *
631 * **Example** (Validating effectfully)
632 *
633 * ```ts import.meta.vitest
634 * import { Effect, Option, SchemaGetter } from "effect"
635 *
636 * const nonNegative = SchemaGetter.checkEffect<number>((n) =>
637 * Effect.succeed(n >= 0 ? undefined : "must be non-negative")
638 * )
639 * await Effect.runPromise(SchemaGetter.run(nonNegative, Option.some(1), {})) // => Option.some(1)
640 * ```
641 *
642 * @see {@link transform} when you need to change the value, not just validate
643 * @see {@link fail} for unconditional failure
644 *
645 * @category validation
646 * @since 4.0.0
647 */
648export function checkEffect<T, R = never>(
649 f: (input: T, options: SchemaAST.ParseOptions) => Effect.Effect<
650 undefined | boolean | Schema.FilterIssue,
651 never,
652 R
653 >
654): Getter<T, T, R> {
655 return transformEffect((t, options) => {
656 return f(t, options).pipe(Effect.flatMapEager((out) => {
657 const issue = SchemaIssue.makeSingle(out, t, options)
658 return issue ?
659 Effect.fail(issue) :
660 Effect.succeed(t)
661 }))
662 })
663}
664
665/**
666 * Creates a getter that applies a pure function to present values.
667 *
668 * **When to use**
669 *
670 * Use when you need a schema getter for a pure, infallible transformation
671 * between types.
672 * - Building encode/decode pairs for `Schema.decodeTo`.
673 *
674 * **Details**
675 *
676 * - This is the most commonly used constructor.
677 * - Transforms `Some(e)` to `Some(f(e))` and leaves `None` unchanged.
678 * - Skips `None` inputs — only called when a value is present.
679 * - Never fails.
680 *
681 * **Example** (Transforming strings to numbers)
682 *
683 * ```ts import.meta.vitest
684 * import { Schema, SchemaGetter } from "effect"
685 *
686 * const NumberFromString = Schema.String.pipe(
687 * Schema.decodeTo(Schema.Number, {
688 * decode: SchemaGetter.transform((s) => Number(s)),
689 * encode: SchemaGetter.transform((n) => String(n))
690 * })
691 * )
692 * Schema.decodeSync(NumberFromString)("42") // => 42
693 * ```
694 *
695 * @see {@link transformEffect} when the transformation returns an `Effect`
696 * @see {@link transformOptional} when you need to handle `None` inputs
697 * @see {@link passthrough} when no transformation is needed
698 *
699 * @category transforming
700 * @since 4.0.0
701 */
702export function transform<T, E>(f: (e: E) => T): Getter<T, E> {
703 return makeGetter({ _tag: "Transform", transform: f })
704}
705
706/**
707 * Creates a getter that applies an effectful transformation to present values.
708 *
709 * **When to use**
710 *
711 * Use when you need a schema getter for a transformation that may fail, require
712 * Effect services, or run asynchronously.
713 *
714 * **Details**
715 *
716 * - Skips `None` inputs — only called when a value is present.
717 * - On success, wraps the result in `Some`.
718 * - On failure, propagates the `Issue`.
719 *
720 * **Example** (Parsing with failure)
721 *
722 * ```ts import.meta.vitest
723 * import { Effect, Option, SchemaGetter, SchemaIssue } from "effect"
724 *
725 * const safeParseInt = SchemaGetter.transformEffect<number, string>(
726 * (s, options) => {
727 * const n = parseInt(s, 10)
728 * return isNaN(n)
729 * ? Effect.fail(new SchemaIssue.InvalidValue({ message: "not an integer" }, s, options))
730 * : Effect.succeed(n)
731 * }
732 * )
733 * await Effect.runPromise(SchemaGetter.run(safeParseInt, Option.some("42"), {})) // => Option.some(42)
734 * ```
735 *
736 * @see {@link transform} when transformation cannot fail
737 * @see {@link transformOptionalEffect} when you need full `Option` control over the output
738 *
739 * @category transforming
740 * @since 4.0.0
741 */
742export function transformEffect<T, E, R = never>(
743 f: (e: E, options: SchemaAST.ParseOptions) => Effect.Effect<T, SchemaIssue.Issue, R>
744): Getter<T, E, R> {
745 return makeGetter({ _tag: "TransformEffect", transform: f })
746}
747
748/**
749 * Creates a getter that transforms the full `Option` — both present and absent values.
750 *
751 * **When to use**
752 *
753 * Use when you need a schema getter to handle both `Some` and `None` cases.
754 *
755 * **Details**
756 *
757 * The getter is pure and never fails. It receives the full `Option<E>` and
758 * must return `Option<T>`, so it can turn a present value into absent or an
759 * absent value into present.
760 *
761 * **Example** (Filtering out empty strings)
762 *
763 * ```ts import.meta.vitest
764 * import { Effect, Option, SchemaGetter } from "effect"
765 *
766 * const skipEmpty = SchemaGetter.transformOptional<string, string>((o) =>
767 * Option.filter(o, (s) => s.length > 0)
768 * )
769 * Effect.runSync(SchemaGetter.run(skipEmpty, Option.some(""), {})) // => Option.none()
770 * ```
771 *
772 * @see {@link transform} when you only need to transform present values
773 * @see {@link omit} when you always want `None`
774 *
775 * @category transforming
776 * @since 4.0.0
777 */
778export function transformOptional<T, E>(f: (oe: Option.Option<E>) => Option.Option<T>): Getter<T, E> {
779 return makeGetter({ _tag: "TransformOptional", transform: f })
780}
781
782/**
783 * Creates a getter that effectfully transforms the full `Option`.
784 *
785 * @category transforming
786 * @since 4.0.0
787 */
788export function transformOptionalEffect<T, E, R = never>(
789 f: (
790 input: Option.Option<E>,
791 options: SchemaAST.ParseOptions
792 ) => Effect.Effect<Option.Option<T>, SchemaIssue.Issue, R>
793): Getter<T, E, R> {
794 return makeGetter({ _tag: "TransformOptionalEffect", transform: f })
795}
796
797/**
798 * Creates a getter that always returns `None`, effectively omitting the value from output.
799 *
800 * **When to use**
801 *
802 * Use when you need a schema getter to exclude a field during decoding or
803 * encoding.
804 *
805 * **Details**
806 *
807 * - Always returns `Option.None` regardless of input.
808 * - Never fails.
809 *
810 * **Example** (Omitting a field during encoding)
811 *
812 * ```ts import.meta.vitest
813 * import { Effect, Option, SchemaGetter } from "effect"
814 *
815 * const omitField = SchemaGetter.omit<string>()
816 * Effect.runSync(SchemaGetter.run(omitField, Option.some("hidden"), {})) // => Option.none()
817 * ```
818 *
819 * @see {@link transformOptional} when you want conditional omission
820 * @see {@link forbidden} when you want to fail instead of silently omit
821 *
822 * @category filtering
823 * @since 4.0.0
824 */
825export function omit<T>(): Getter<never, T> {
826 return transformOptional(() => Option.none())
827}
828
829/**
830 * Creates a getter that replaces `undefined` values with a default.
831 *
832 * **When to use**
833 *
834 * Use when you need a schema getter to provide a fallback for a field that may
835 * be `undefined` in the encoded input.
836 *
837 * **Details**
838 *
839 * - If the input is `Some(undefined)` or `None`, produces `Some(T)`.
840 * - If the input is `Some(value)` where value is not `undefined`, passes it through.
841 * - `defaultValue` is an `Effect` that will be executed each time a default is needed.
842 *
843 * **Example** (Providing a default value for an optional field)
844 *
845 * ```ts import.meta.vitest
846 * import { Effect, Option, SchemaGetter } from "effect"
847 *
848 * const withZero = SchemaGetter.withDefault(Effect.succeed(0))
849 * await Effect.runPromise(SchemaGetter.run(withZero, Option.some(undefined), {})) // => Option.some(0)
850 * ```
851 *
852 * @see {@link transformOptionalEffect} for custom effectful missing-key handling
853 * @see {@link required} when absent input should fail instead of using a default
854 *
855 * @category transforming
856 * @since 4.0.0
857 */
858export function withDefault<T, R = never>(
859 defaultValue: Effect.Effect<T, SchemaIssue.Issue, R>
860): Getter<T, T | undefined, R> {
861 return transformOptionalEffect((o) => {
862 const filtered = Option.filter(o, Predicate.isNotUndefined)
863 return Option.isSome(filtered) ? Effect.succeed(filtered) : Effect.mapEager(defaultValue, Option.some)
864 })
865}
866
867/**
868 * Coerces any value to a `string` using the global `String()` constructor.
869 *
870 * **When to use**
871 *
872 * Use when you need a schema getter to coerce a present encoded value to a
873 * string with `String()`.
874 *
875 * **Details**
876 *
877 * The getter is pure, never fails, and delegates to `globalThis.String`.
878 *
879 * **Example** (Coercing to a string)
880 *
881 * ```ts import.meta.vitest
882 * import { Effect, Option, SchemaGetter } from "effect"
883 *
884 * const toString = SchemaGetter.String<number>()
885 * Effect.runSync(SchemaGetter.run(toString, Option.some(42), {})) // => Option.some("42")
886 * ```
887 *
888 * @see {@link transform} for custom string conversions
889 *
890 * @category converting
891 * @since 4.0.0
892 */
893export function String<E>(): Getter<string, E> {
894 return transform(globalThis.String)
895}
896
897/**
898 * Coerces any value to a `number` using the global `Number()` constructor.
899 *
900 * **When to use**
901 *
902 * Use when you need a schema getter to coerce a present encoded value to a
903 * number with `Number()`.
904 *
905 * **Details**
906 *
907 * The getter is pure, never fails, and delegates to `globalThis.Number`. It may
908 * produce `NaN` for non-numeric inputs.
909 *
910 * **Example** (Coercing to a number)
911 *
912 * ```ts import.meta.vitest
913 * import { Effect, Option, SchemaGetter } from "effect"
914 *
915 * const toNumber = SchemaGetter.Number<string>()
916 * Effect.runSync(SchemaGetter.run(toNumber, Option.some("42"), {})) // => Option.some(42)
917 * ```
918 *
919 * @see {@link transformEffect} for effectful or validated number parsing
920 *
921 * @category converting
922 * @since 4.0.0
923 */
924export function Number<E>(): Getter<number, E> {
925 return transform(globalThis.Number)
926}
927
928/**
929 * Coerces any value to a `boolean` using the global `Boolean()` constructor.
930 *
931 * **When to use**
932 *
933 * Use when you need a schema getter to coerce a present encoded value to a
934 * boolean with `Boolean()`.
935 *
936 * **Details**
937 *
938 * The getter is pure, never fails, and delegates to `globalThis.Boolean`.
939 *
940 * **Example** (Coercing to a boolean)
941 *
942 * ```ts import.meta.vitest
943 * import { Effect, Option, SchemaGetter } from "effect"
944 *
945 * const toBool = SchemaGetter.Boolean<string>()
946 * Effect.runSync(SchemaGetter.run(toBool, Option.some("true"), {})) // => Option.some(true)
947 * ```
948 *
949 * @category converting
950 * @since 4.0.0
951 */
952export function Boolean<E>(): Getter<boolean, E> {
953 return transform(globalThis.Boolean)
954}
955
956/**
957 * Coerces a value to `bigint` using the global `BigInt()` constructor.
958 *
959 * **When to use**
960 *
961 * Use when you need a schema getter to convert a present string, number, or
962 * boolean value to `bigint`.
963 *
964 * **Details**
965 *
966 * - Delegates to `globalThis.BigInt`.
967 * - Throws at runtime if the input cannot be converted (e.g. non-numeric string).
968 *
969 * **Example** (Coercing to a bigint)
970 *
971 * ```ts import.meta.vitest
972 * import { Effect, Option, SchemaGetter } from "effect"
973 *
974 * const toBigInt = SchemaGetter.BigInt<string>()
975 * Effect.runSync(SchemaGetter.run(toBigInt, Option.some("42"), {})) // => Option.some(42n)
976 * ```
977 *
978 * @category converting
979 * @since 4.0.0
980 */
981export function BigInt<E extends string | number | bigint | boolean>(): Getter<bigint, E> {
982 return transform(globalThis.BigInt)
983}
984
985/**
986 * Coerces a value to a `Date` using `new Date(input)`.
987 *
988 * **When to use**
989 *
990 * Use when you need a schema getter to coerce a present string, number, or
991 * existing date object into a new date object.
992 *
993 * **Details**
994 *
995 * - Delegates to `new globalThis.Date(input)`.
996 * - Does not validate the result — may produce an invalid Date.
997 *
998 * **Example** (Coercing to a Date)
999 *
1000 * ```ts import.meta.vitest
1001 * import { Effect, Option, SchemaGetter } from "effect"
1002 *
1003 * const toDate = SchemaGetter.Date<string>()
1004 * const result = Effect.runSync(SchemaGetter.run(toDate, Option.some("1970-01-01"), {}))
1005 * Option.map(result, (date) => date.toISOString()) // => Option.some("1970-01-01T00:00:00.000Z")
1006 * ```
1007 *
1008 * @see {@link dateTimeUtcFromInput} for validated DateTime parsing
1009 *
1010 * @category converting
1011 * @since 4.0.0
1012 */
1013export function Date<E extends string | number | Date>(): Getter<Date, E> {
1014 return transform((u) => new globalThis.Date(u))
1015}
1016
1017/**
1018 * Strips whitespace from both ends of a string.
1019 *
1020 * **Details**
1021 *
1022 * - Pure, delegates to `String.trim`.
1023 *
1024 * **Example** (Trimming whitespace)
1025 *
1026 * ```ts import.meta.vitest
1027 * import { Effect, Option, SchemaGetter } from "effect"
1028 *
1029 * const trimmed = SchemaGetter.trim<string>()
1030 * Effect.runSync(SchemaGetter.run(trimmed, Option.some(" hello "), {})) // => Option.some("hello")
1031 * ```
1032 *
1033 * @category transforming
1034 * @since 4.0.0
1035 */
1036export function trim<E extends string>(): Getter<string, E> {
1037 return transform(Str.trim)
1038}
1039
1040/**
1041 * Capitalizes the first character of a string.
1042 *
1043 * **Details**
1044 *
1045 * - Pure, delegates to `String.capitalize`.
1046 *
1047 * **Example** (Capitalizing a string)
1048 *
1049 * ```ts import.meta.vitest
1050 * import { Effect, Option, SchemaGetter } from "effect"
1051 *
1052 * const cap = SchemaGetter.capitalize<string>()
1053 * Effect.runSync(SchemaGetter.run(cap, Option.some("hello"), {})) // => Option.some("Hello")
1054 * ```
1055 *
1056 * @category transforming
1057 * @since 4.0.0
1058 */
1059export function capitalize<E extends string>(): Getter<string, E> {
1060 return transform(Str.capitalize)
1061}
1062
1063/**
1064 * Uncapitalizes the first character of a string.
1065 *
1066 * **Details**
1067 *
1068 * - Pure, delegates to `String.uncapitalize`.
1069 *
1070 * **Example** (Uncapitalizing a string)
1071 *
1072 * ```ts import.meta.vitest
1073 * import { Effect, Option, SchemaGetter } from "effect"
1074 *
1075 * const uncap = SchemaGetter.uncapitalize<string>()
1076 * Effect.runSync(SchemaGetter.run(uncap, Option.some("Hello"), {})) // => Option.some("hello")
1077 * ```
1078 *
1079 * @category transforming
1080 * @since 4.0.0
1081 */
1082export function uncapitalize<E extends string>(): Getter<string, E> {
1083 return transform(Str.uncapitalize)
1084}
1085
1086/**
1087 * Converts a `snake_case` string to `camelCase`.
1088 *
1089 * **Details**
1090 *
1091 * - Pure, delegates to `String.snakeToCamel`.
1092 *
1093 * **Example** (Converting snake case to camel case)
1094 *
1095 * ```ts import.meta.vitest
1096 * import { Effect, Option, SchemaGetter } from "effect"
1097 *
1098 * const toCamel = SchemaGetter.snakeToCamel<string>()
1099 * Effect.runSync(SchemaGetter.run(toCamel, Option.some("user_name"), {})) // => Option.some("userName")
1100 * ```
1101 *
1102 * @see {@link camelToSnake} for the inverse operation
1103 *
1104 * @category transforming
1105 * @since 4.0.0
1106 */
1107export function snakeToCamel<E extends string>(): Getter<string, E> {
1108 return transform(Str.snakeToCamel)
1109}
1110
1111/**
1112 * Converts a `camelCase` string to `snake_case`.
1113 *
1114 * **Details**
1115 *
1116 * - Pure, delegates to `String.camelToSnake`.
1117 *
1118 * **Example** (Converting camel case to snake case)
1119 *
1120 * ```ts import.meta.vitest
1121 * import { Effect, Option, SchemaGetter } from "effect"
1122 *
1123 * const toSnake = SchemaGetter.camelToSnake<string>()
1124 * Effect.runSync(SchemaGetter.run(toSnake, Option.some("userName"), {})) // => Option.some("user_name")
1125 * ```
1126 *
1127 * @see {@link snakeToCamel} for the inverse operation
1128 *
1129 * @category transforming
1130 * @since 4.0.0
1131 */
1132export function camelToSnake<E extends string>(): Getter<string, E> {
1133 return transform(Str.camelToSnake)
1134}
1135
1136/**
1137 * Converts a string to lowercase.
1138 *
1139 * **Details**
1140 *
1141 * - Pure, delegates to `String.toLowerCase`.
1142 *
1143 * **Example** (Converting to lowercase)
1144 *
1145 * ```ts import.meta.vitest
1146 * import { Effect, Option, SchemaGetter } from "effect"
1147 *
1148 * const lower = SchemaGetter.toLowerCase<string>()
1149 * Effect.runSync(SchemaGetter.run(lower, Option.some("HELLO"), {})) // => Option.some("hello")
1150 * ```
1151 *
1152 * @see {@link toUpperCase} for the inverse operation
1153 *
1154 * @category transforming
1155 * @since 4.0.0
1156 */
1157export function toLowerCase<E extends string>(): Getter<string, E> {
1158 return transform(Str.toLowerCase)
1159}
1160
1161/**
1162 * Converts a string to uppercase.
1163 *
1164 * **Details**
1165 *
1166 * - Pure, delegates to `String.toUpperCase`.
1167 *
1168 * **Example** (Converting to uppercase)
1169 *
1170 * ```ts import.meta.vitest
1171 * import { Effect, Option, SchemaGetter } from "effect"
1172 *
1173 * const upper = SchemaGetter.toUpperCase<string>()
1174 * Effect.runSync(SchemaGetter.run(upper, Option.some("hello"), {})) // => Option.some("HELLO")
1175 * ```
1176 *
1177 * @see {@link toLowerCase} for the inverse operation
1178 *
1179 * @category transforming
1180 * @since 4.0.0
1181 */
1182export function toUpperCase<E extends string>(): Getter<string, E> {
1183 return transform(Str.toUpperCase)
1184}
1185
1186type ParseJsonOptions = {
1187 readonly reviver?: Parameters<typeof JSON.parse>[1]
1188}
1189
1190/**
1191 * Parses a JSON string into a value.
1192 *
1193 * **When to use**
1194 *
1195 * Use when you need a schema getter to parse a present encoded JSON string
1196 * during decoding.
1197 *
1198 * **Details**
1199 *
1200 * - Skips `None` inputs.
1201 * - Without `reviver`: returns `Schema.MutableJson` (typed JSON).
1202 * - With `reviver`: returns `unknown` (reviver may produce arbitrary values).
1203 * - On parse failure, fails with `SchemaIssue.InvalidValue` whose `expected`
1204 * annotation is `"a valid JSON string"`. Its default message includes the
1205 * reported input when `reportInput` is enabled.
1206 *
1207 * **Example** (Parsing JSON)
1208 *
1209 * ```ts import.meta.vitest
1210 * import { Effect, Option, SchemaGetter } from "effect"
1211 *
1212 * const parse = SchemaGetter.parseJson<string>()
1213 * const result = await Effect.runPromise(SchemaGetter.run(parse, Option.some("{\"a\":1}"), {}))
1214 * result // => Option.some({ a: 1 })
1215 * ```
1216 *
1217 * @see {@link stringifyJson} for the inverse operation
1218 *
1219 * @category decoding
1220 * @since 4.0.0
1221 */
1222export function parseJson<E extends string>(): Getter<Schema.MutableJson, E>
1223export function parseJson<E extends string>(options: ParseJsonOptions): Getter<unknown, E>
1224export function parseJson<E extends string>(options?: ParseJsonOptions | undefined): Getter<unknown, E> {
1225 return transformEffect((input, parseOptions) =>
1226 Effect.try({
1227 try: () => JSON.parse(input, options?.reviver),
1228 catch: () =>
1229 new SchemaIssue.InvalidValue(
1230 { expected: "a valid JSON string" },
1231 input,
1232 parseOptions
1233 )
1234 })
1235 )
1236}
1237
1238/**
1239 * Replacer function or property allowlist accepted by `JSON.stringify`.
1240 *
1241 * @category utility types
1242 * @since 4.0.0
1243 */
1244export type JsonReplacer =
1245 | ((this: any, key: string, value: any) => any)
1246 | Array<string | number>
1247 | null
1248
1249type StringifyJsonOptions = {
1250 readonly replacer?: JsonReplacer | undefined
1251 readonly space?: Parameters<typeof JSON.stringify>[2]
1252}
1253
1254/**
1255 * Stringifies a present value using `JSON.stringify`.
1256 *
1257 * **When to use**
1258 *
1259 * Use when you need a schema getter to serialize a present decoded value to
1260 * JSON text during encoding.
1261 *
1262 * **Details**
1263 *
1264 * - Skips `None` inputs.
1265 * - If `JSON.stringify` throws or returns `undefined`, fails with
1266 * `SchemaIssue.InvalidValue`.
1267 * - Supports optional `replacer` and `space` options, matching
1268 * `JSON.stringify`.
1269 *
1270 * **Example** (Stringifying JSON)
1271 *
1272 * ```ts import.meta.vitest
1273 * import { Effect, Option, SchemaGetter } from "effect"
1274 *
1275 * const stringify = SchemaGetter.stringifyJson()
1276 * const result = await Effect.runPromise(SchemaGetter.run(stringify, Option.some({ a: 1 }), {}))
1277 * result // => Option.some("{\"a\":1}")
1278 * ```
1279 *
1280 * @see {@link parseJson} for the inverse operation
1281 *
1282 * @category encoding
1283 * @since 4.0.0
1284 */
1285export function stringifyJson(options?: StringifyJsonOptions): Getter<string, unknown> {
1286 return transformEffect((input, parseOptions) =>
1287 Effect.try({
1288 try: () => {
1289 const output = JSON.stringify(input, options?.replacer as any, options?.space)
1290 if (output === undefined) {
1291 throw new TypeError("Value cannot be represented as JSON")
1292 }
1293 return output
1294 },
1295 catch: () =>
1296 new SchemaIssue.InvalidValue(
1297 { expected: "a JSON-serializable value" },
1298 input,
1299 parseOptions
1300 )
1301 })
1302 )
1303}
1304
1305/**
1306 * Parses a string into a record of key-value pairs.
1307 *
1308 * **When to use**
1309 *
1310 * Use when you need a schema getter to parse a present encoded string that
1311 * contains delimited key-value pairs (e.g. `"a=1,b=2"`).
1312 *
1313 * **Details**
1314 *
1315 * The getter is pure and never fails. It splits the string by `separator`
1316 * (default `,`) and then each pair by `keyValueSeparator` (default `=`). Pairs
1317 * missing a key or value are silently skipped.
1318 *
1319 * **Example** (Parsing a key-value string)
1320 *
1321 * ```ts import.meta.vitest
1322 * import { Effect, Option, SchemaGetter } from "effect"
1323 *
1324 * const parse = SchemaGetter.splitKeyValue<string>()
1325 * const result = Effect.runSync(SchemaGetter.run(parse, Option.some("a=1,b=2"), {}))
1326 * result // => Option.some({ a: "1", b: "2" })
1327 * ```
1328 *
1329 * @see {@link joinKeyValue} for the inverse operation
1330 * @see {@link split} to split into an array of strings
1331 *
1332 * @category splitting
1333 * @since 4.0.0
1334 */
1335export function splitKeyValue<E extends string>(options?: {
1336 readonly separator?: string | undefined
1337 readonly keyValueSeparator?: string | undefined
1338}): Getter<Record<string, string>, E> {
1339 const separator = options?.separator ?? ","
1340 const keyValueSeparator = options?.keyValueSeparator ?? "="
1341 return transform((input) =>
1342 input.split(separator).reduce((acc, pair) => {
1343 const [key, value] = pair.split(keyValueSeparator)
1344 if (key && value) {
1345 InternalRecord.assignProperty(acc, key, value)
1346 }
1347 return acc
1348 }, {} as Record<string, string>)
1349 )
1350}
1351
1352/**
1353 * Joins a record of key-value pairs into a delimited string.
1354 *
1355 * **When to use**
1356 *
1357 * Use when you need a schema getter to serialize a present decoded record as a
1358 * delimited key-value string.
1359 *
1360 * **Details**
1361 *
1362 * The getter is pure and never fails. It joins entries with `separator`
1363 * (default `,`) and joins each key and value with `keyValueSeparator` (default
1364 * `=`).
1365 *
1366 * **Example** (Joining key-value records)
1367 *
1368 * ```ts import.meta.vitest
1369 * import { Effect, Option, SchemaGetter } from "effect"
1370 *
1371 * const join = SchemaGetter.joinKeyValue()
1372 * const result = Effect.runSync(SchemaGetter.run(join, Option.some({ a: "1", b: "2" }), {}))
1373 * result // => Option.some("a=1,b=2")
1374 * ```
1375 *
1376 * @see {@link splitKeyValue} for the inverse operation
1377 *
1378 * @category combining
1379 * @since 4.0.0
1380 */
1381export function joinKeyValue<E extends Record<PropertyKey, string>>(options?: {
1382 readonly separator?: string | undefined
1383 readonly keyValueSeparator?: string | undefined
1384}): Getter<string, E> {
1385 const separator = options?.separator ?? ","
1386 const keyValueSeparator = options?.keyValueSeparator ?? "="
1387 return transform((input) =>
1388 Object.entries(input).map(([key, value]) => `${key}${keyValueSeparator}${value}`).join(separator)
1389 )
1390}
1391
1392/**
1393 * Splits a string into an array of strings by a separator.
1394 *
1395 * **When to use**
1396 *
1397 * Use when you need a schema getter to split a present encoded string
1398 * containing a delimited list, such as CSV values.
1399 *
1400 * **Details**
1401 *
1402 * The getter is pure and never fails. It splits by `separator` (default `,`).
1403 * An empty string produces an empty array, not `[""]`.
1404 *
1405 * **Example** (Splitting a comma-separated string)
1406 *
1407 * ```ts import.meta.vitest
1408 * import { Effect, Option, SchemaGetter } from "effect"
1409 *
1410 * const splitComma = SchemaGetter.split<string>()
1411 * const result = Effect.runSync(SchemaGetter.run(splitComma, Option.some("a,b,c"), {}))
1412 * result // => Option.some(["a", "b", "c"])
1413 * ```
1414 *
1415 * @see {@link splitKeyValue} when values are key-value pairs
1416 *
1417 * @category splitting
1418 * @since 4.0.0
1419 */
1420export function split<E extends string>(options?: {
1421 readonly separator?: string | undefined
1422}): Getter<ReadonlyArray<string>, E> {
1423 const separator = options?.separator ?? ","
1424 return transform((input) => input === "" ? [] : input.split(separator))
1425}
1426
1427/**
1428 * Encodes a `Uint8Array` or string to a Base64 string.
1429 *
1430 * **Details**
1431 *
1432 * The getter is pure and never fails.
1433 *
1434 * **Example** (Encoding to Base64)
1435 *
1436 * ```ts import.meta.vitest
1437 * import { Effect, Option, SchemaGetter } from "effect"
1438 *
1439 * const encode = SchemaGetter.encodeBase64<Uint8Array>()
1440 * const result = Effect.runSync(SchemaGetter.run(encode, Option.some(new Uint8Array([1, 2, 3])), {}))
1441 * result // => Option.some("AQID")
1442 * ```
1443 *
1444 * @see {@link decodeBase64} for the inverse operation to `Uint8Array`
1445 * @see {@link decodeBase64String} for the inverse operation to `string`
1446 * @see {@link encodeBase64Url} for the URL-safe variant
1447 *
1448 * @category encoding
1449 * @since 4.0.0
1450 */
1451export function encodeBase64<E extends Uint8Array | string>(): Getter<string, E> {
1452 return transform(Base64.encode)
1453}
1454
1455/**
1456 * Encodes a `Uint8Array` or string to a URL-safe Base64 string.
1457 *
1458 * **Details**
1459 *
1460 * The getter is pure and never fails.
1461 *
1462 * **Example** (Encoding to Base64Url)
1463 *
1464 * ```ts import.meta.vitest
1465 * import { Effect, Option, SchemaGetter } from "effect"
1466 *
1467 * const encode = SchemaGetter.encodeBase64Url<Uint8Array>()
1468 * const result = Effect.runSync(SchemaGetter.run(encode, Option.some(new Uint8Array([251, 255])), {}))
1469 * result // => Option.some("-_8")
1470 * ```
1471 *
1472 * @see {@link decodeBase64Url} for the inverse operation to `Uint8Array`
1473 * @see {@link decodeBase64UrlString} for the inverse operation to `string`
1474 * @see {@link encodeBase64} for the standard Base64 variant
1475 *
1476 * @category encoding
1477 * @since 4.0.0
1478 */
1479export function encodeBase64Url<E extends Uint8Array | string>(): Getter<string, E> {
1480 return transform(Base64Url.encode)
1481}
1482
1483/**
1484 * Encodes a `Uint8Array` or string to a hexadecimal string.
1485 *
1486 * **Details**
1487 *
1488 * The getter is pure and never fails.
1489 *
1490 * **Example** (Encoding to hex)
1491 *
1492 * ```ts import.meta.vitest
1493 * import { Effect, Option, SchemaGetter } from "effect"
1494 *
1495 * const encode = SchemaGetter.encodeHex<Uint8Array>()
1496 * const result = Effect.runSync(SchemaGetter.run(encode, Option.some(new Uint8Array([1, 2, 3])), {}))
1497 * result // => Option.some("010203")
1498 * ```
1499 *
1500 * @see {@link decodeHex} for the inverse operation to `Uint8Array`
1501 * @see {@link decodeHexString} for the inverse operation to `string`
1502 *
1503 * @category encoding
1504 * @since 4.0.0
1505 */
1506export function encodeHex<E extends Uint8Array | string>(): Getter<string, E> {
1507 return transform(Hex.encode)
1508}
1509
1510/**
1511 * Decodes a Base64 string to a `Uint8Array`.
1512 *
1513 * **Details**
1514 *
1515 * - Fails with `SchemaIssue.InvalidValue` if the input is not valid Base64.
1516 *
1517 * **Example** (Decoding Base64 to bytes)
1518 *
1519 * ```ts import.meta.vitest
1520 * import { Effect, Option, SchemaGetter } from "effect"
1521 *
1522 * const decode = SchemaGetter.decodeBase64<string>()
1523 * const result = await Effect.runPromise(SchemaGetter.run(decode, Option.some("AQID"), {}))
1524 * Option.map(result, Array.from) // => Option.some([1, 2, 3])
1525 * ```
1526 *
1527 * @see {@link decodeBase64String} to decode to `string` instead
1528 * @see {@link encodeBase64} for the inverse operation
1529 *
1530 * @category decoding
1531 * @since 4.0.0
1532 */
1533export function decodeBase64<E extends string>(): Getter<Uint8Array, E> {
1534 return transformEffect((input, options) =>
1535 Effect.mapErrorEager(
1536 Effect.fromResult(Base64.decode(input)),
1537 () =>
1538 new SchemaIssue.InvalidValue(
1539 { expected: "a valid Base64 string" },
1540 input,
1541 options
1542 )
1543 )
1544 )
1545}
1546
1547/**
1548 * Decodes a Base64 string to a UTF-8 `string`.
1549 *
1550 * **Details**
1551 *
1552 * - Fails with `SchemaIssue.InvalidValue` if the input is not valid Base64.
1553 *
1554 * **Example** (Decoding Base64 to string)
1555 *
1556 * ```ts import.meta.vitest
1557 * import { Effect, Option, SchemaGetter } from "effect"
1558 *
1559 * const decode = SchemaGetter.decodeBase64String<string>()
1560 * const result = await Effect.runPromise(SchemaGetter.run(decode, Option.some("aGVsbG8="), {}))
1561 * result // => Option.some("hello")
1562 * ```
1563 *
1564 * @see {@link decodeBase64} to decode to `Uint8Array` instead
1565 * @see {@link encodeBase64} for the inverse operation
1566 *
1567 * @category decoding
1568 * @since 4.0.0
1569 */
1570export function decodeBase64String<E extends string>(): Getter<string, E> {
1571 return transformEffect((input, options) =>
1572 Result.match(Base64.decodeString(input), {
1573 onFailure: () =>
1574 Effect.fail(
1575 new SchemaIssue.InvalidValue(
1576 { expected: "a valid Base64 string" },
1577 input,
1578 options
1579 )
1580 ),
1581 onSuccess: Effect.succeed
1582 })
1583 )
1584}
1585
1586/**
1587 * Decodes a URL-safe Base64 string to a `Uint8Array`.
1588 *
1589 * **Details**
1590 *
1591 * - Fails with `SchemaIssue.InvalidValue` if the input is not valid Base64Url.
1592 *
1593 * **Example** (Decoding Base64Url to bytes)
1594 *
1595 * ```ts import.meta.vitest
1596 * import { Effect, Option, SchemaGetter } from "effect"
1597 *
1598 * const decode = SchemaGetter.decodeBase64Url<string>()
1599 * const result = await Effect.runPromise(SchemaGetter.run(decode, Option.some("-_8="), {}))
1600 * Option.map(result, Array.from) // => Option.some([251, 255])
1601 * ```
1602 *
1603 * @see {@link decodeBase64UrlString} to decode to `string` instead
1604 * @see {@link encodeBase64Url} for the inverse operation
1605 *
1606 * @category decoding
1607 * @since 4.0.0
1608 */
1609export function decodeBase64Url<E extends string>(): Getter<Uint8Array, E> {
1610 return transformEffect((input, options) =>
1611 Result.match(Base64Url.decode(input), {
1612 onFailure: () =>
1613 Effect.fail(
1614 new SchemaIssue.InvalidValue(
1615 { expected: "a valid Base64Url string" },
1616 input,
1617 options
1618 )
1619 ),
1620 onSuccess: Effect.succeed
1621 })
1622 )
1623}
1624
1625/**
1626 * Decodes a URL-safe Base64 string to a UTF-8 `string`.
1627 *
1628 * **Details**
1629 *
1630 * - Fails with `SchemaIssue.InvalidValue` if the input is not valid Base64Url.
1631 *
1632 * **Example** (Decoding Base64Url to string)
1633 *
1634 * ```ts import.meta.vitest
1635 * import { Effect, Option, SchemaGetter } from "effect"
1636 *
1637 * const decode = SchemaGetter.decodeBase64UrlString<string>()
1638 * const result = await Effect.runPromise(SchemaGetter.run(decode, Option.some("aGVsbG8"), {}))
1639 * result // => Option.some("hello")
1640 * ```
1641 *
1642 * @see {@link decodeBase64Url} to decode to `Uint8Array` instead
1643 * @see {@link encodeBase64Url} for the inverse operation
1644 *
1645 * @category decoding
1646 * @since 4.0.0
1647 */
1648export function decodeBase64UrlString<E extends string>(): Getter<string, E> {
1649 return transformEffect((input, options) =>
1650 Result.match(Base64Url.decodeString(input), {
1651 onFailure: () =>
1652 Effect.fail(
1653 new SchemaIssue.InvalidValue(
1654 { expected: "a valid Base64Url string" },
1655 input,
1656 options
1657 )
1658 ),
1659 onSuccess: Effect.succeed
1660 })
1661 )
1662}
1663
1664/**
1665 * Decodes a hexadecimal string to a `Uint8Array`.
1666 *
1667 * **Details**
1668 *
1669 * - Fails with `SchemaIssue.InvalidValue` if the input is not valid hex.
1670 *
1671 * **Example** (Decoding hex to bytes)
1672 *
1673 * ```ts import.meta.vitest
1674 * import { Effect, Option, SchemaGetter } from "effect"
1675 *
1676 * const decode = SchemaGetter.decodeHex<string>()
1677 * const result = await Effect.runPromise(SchemaGetter.run(decode, Option.some("010203"), {}))
1678 * Option.map(result, Array.from) // => Option.some([1, 2, 3])
1679 * ```
1680 *
1681 * @see {@link decodeHexString} to decode to `string` instead
1682 * @see {@link encodeHex} for the inverse operation
1683 *
1684 * @category decoding
1685 * @since 4.0.0
1686 */
1687export function decodeHex<E extends string>(): Getter<Uint8Array, E> {
1688 return transformEffect((input, options) =>
1689 Result.match(Hex.decode(input), {
1690 onFailure: () =>
1691 Effect.fail(
1692 new SchemaIssue.InvalidValue(
1693 { expected: "a valid hexadecimal string" },
1694 input,
1695 options
1696 )
1697 ),
1698 onSuccess: Effect.succeed
1699 })
1700 )
1701}
1702
1703/**
1704 * Decodes a hexadecimal string to a UTF-8 `string`.
1705 *
1706 * **Details**
1707 *
1708 * - Fails with `SchemaIssue.InvalidValue` if the input is not valid hex.
1709 *
1710 * **Example** (Decoding hex to string)
1711 *
1712 * ```ts import.meta.vitest
1713 * import { Effect, Option, SchemaGetter } from "effect"
1714 *
1715 * const decode = SchemaGetter.decodeHexString<string>()
1716 * const result = await Effect.runPromise(SchemaGetter.run(decode, Option.some("68656c6c6f"), {}))
1717 * result // => Option.some("hello")
1718 * ```
1719 *
1720 * @see {@link decodeHex} to decode to `Uint8Array` instead
1721 * @see {@link encodeHex} for the inverse operation
1722 *
1723 * @category decoding
1724 * @since 4.0.0
1725 */
1726export function decodeHexString<E extends string>(): Getter<string, E> {
1727 return transformEffect((input, options) =>
1728 Result.match(Hex.decodeString(input), {
1729 onFailure: () =>
1730 Effect.fail(
1731 new SchemaIssue.InvalidValue(
1732 { expected: "a valid hexadecimal string" },
1733 input,
1734 options
1735 )
1736 ),
1737 onSuccess: Effect.succeed
1738 })
1739 )
1740}
1741
1742/**
1743 * Encodes a present string using `encodeURIComponent`.
1744 *
1745 * **Details**
1746 *
1747 * - Skips `None` inputs.
1748 * - May throw a `URIError` for malformed surrogate pairs; this exception is not
1749 * converted into an `Issue`.
1750 *
1751 * **Example** (Encoding a URI component)
1752 *
1753 * ```ts import.meta.vitest
1754 * import { Effect, Option, SchemaGetter } from "effect"
1755 *
1756 * const encode = SchemaGetter.encodeUriComponent<string>()
1757 * const result = Effect.runSync(SchemaGetter.run(encode, Option.some("hello world"), {}))
1758 * result // => Option.some("hello%20world")
1759 * ```
1760 *
1761 * @see {@link decodeUriComponent} for the inverse operation
1762 *
1763 * @category encoding
1764 * @since 4.0.0
1765 */
1766export function encodeUriComponent<E extends string>(): Getter<string, E> {
1767 return transform(encodeURIComponent)
1768}
1769
1770/**
1771 * Decodes a URI component encoded string using `decodeURIComponent`.
1772 *
1773 * **Details**
1774 *
1775 * - Fails with `SchemaIssue.InvalidValue` if the input contains malformed percent-encoding sequences.
1776 *
1777 * **Example** (Decoding a URI component)
1778 *
1779 * ```ts import.meta.vitest
1780 * import { Effect, Option, SchemaGetter } from "effect"
1781 *
1782 * const decode = SchemaGetter.decodeUriComponent<string>()
1783 * const result = await Effect.runPromise(SchemaGetter.run(decode, Option.some("hello%20world"), {}))
1784 * result // => Option.some("hello world")
1785 * ```
1786 *
1787 * @see {@link encodeUriComponent} for the inverse operation
1788 *
1789 * @category decoding
1790 * @since 4.0.0
1791 */
1792export function decodeUriComponent<E extends string>(): Getter<string, E> {
1793 return transformEffect((input, options) => {
1794 try {
1795 return Effect.succeed(globalThis.decodeURIComponent(input))
1796 } catch {
1797 return Effect.fail(
1798 new SchemaIssue.InvalidValue(
1799 { expected: "a valid URI component" },
1800 input,
1801 options
1802 )
1803 )
1804 }
1805 })
1806}
1807
1808/**
1809 * Parses a `DateTime.Input` value into a `DateTime.Utc`.
1810 *
1811 * **When to use**
1812 *
1813 * Use when you need a schema getter to decode a present encoded date/time value
1814 * to a `DateTime.Utc`.
1815 *
1816 * **Details**
1817 *
1818 * - Accepted input includes existing `DateTime` values, partial date/time parts,
1819 * instant objects, zoned instant objects, JavaScript `Date` instances, epoch
1820 * milliseconds, and date strings.
1821 * - Converts successfully parsed values to UTC.
1822 * - Fails with `SchemaIssue.InvalidValue` if the input cannot be parsed as a valid
1823 * `DateTime`.
1824 *
1825 * **Example** (Parsing DateTime)
1826 *
1827 * ```ts import.meta.vitest
1828 * import { DateTime, Effect, Option, SchemaGetter } from "effect"
1829 *
1830 * const parseDate = SchemaGetter.dateTimeUtcFromInput<string>()
1831 * const result = await Effect.runPromise(
1832 * SchemaGetter.run(parseDate, Option.some("2024-01-01T00:00:00Z"), {})
1833 * )
1834 * Option.map(result, DateTime.toEpochMillis) // => Option.some(1704067200000)
1835 * ```
1836 *
1837 * @see {@link Date} for a simpler coercion to `Date` (no validation)
1838 *
1839 * @category converting
1840 * @since 4.0.0
1841 */
1842export function dateTimeUtcFromInput<E extends DateTime.DateTime.Input>(): Getter<DateTime.Utc, E> {
1843 return transformEffect((input, options) => {
1844 return Option.match(DateTime.make(input), {
1845 onNone: () =>
1846 Effect.fail(
1847 new SchemaIssue.InvalidValue({ message: "Invalid DateTime input" }, input, options)
1848 ),
1849 onSome: (dt) => Effect.succeed(DateTime.toUtc(dt))
1850 })
1851 })
1852}
1853
1854/**
1855 * Decodes a `FormData` object into a nested tree structure using bracket-path notation.
1856 *
1857 * **When to use**
1858 *
1859 * Use when you need a schema getter to parse `FormData` from HTTP requests into
1860 * structured objects.
1861 *
1862 * **Details**
1863 *
1864 * The getter is pure and never fails. It interprets bracket-path keys such as
1865 * `user[name]` and `items[0]` to build nested objects or arrays, and each leaf
1866 * value is a `string` or `Blob`.
1867 *
1868 * **Example** (Decoding FormData)
1869 *
1870 * ```ts import.meta.vitest
1871 * import { Effect, Option, SchemaGetter } from "effect"
1872 *
1873 * const decode = SchemaGetter.decodeFormData()
1874 * const formData = new FormData()
1875 * formData.append("user[name]", "Alice")
1876 * const result = Effect.runSync(SchemaGetter.run(decode, Option.some(formData), {}))
1877 * result // => Option.some({ user: { name: "Alice" } })
1878 * ```
1879 *
1880 * @see {@link encodeFormData} for the corresponding encoder
1881 * @see {@link makeTreeRecord} for the underlying bracket-path parser
1882 * @see {@link decodeURLSearchParams} for the URLSearchParams variant
1883 *
1884 * @category decoding
1885 * @since 4.0.0
1886 */
1887export function decodeFormData(): Getter<Schema.TreeRecord<string | Blob>, FormData> {
1888 return transform((input) => makeTreeRecord(Array.from(input.entries())))
1889}
1890
1891const collectFormDataEntries = collectBracketPathEntries((value): value is string | Blob =>
1892 typeof value === "string" || (typeof Blob !== "undefined" && value instanceof Blob)
1893)
1894
1895/**
1896 * Encodes a nested object into a `FormData` instance using bracket-path notation.
1897 *
1898 * **When to use**
1899 *
1900 * Use when you need a schema getter to serialize structured data to `FormData`
1901 * for HTTP requests.
1902 *
1903 * **Details**
1904 *
1905 * The getter is pure and never fails. It flattens nested objects or arrays into
1906 * bracket-path keys such as `user[name]` and `items[0]`. Non-object inputs
1907 * produce an empty `FormData`.
1908 *
1909 * **Example** (Encoding to FormData)
1910 *
1911 * ```ts import.meta.vitest
1912 * import { Effect, Option, SchemaGetter } from "effect"
1913 *
1914 * const encode = SchemaGetter.encodeFormData()
1915 * const result = Effect.runSync(SchemaGetter.run(encode, Option.some({ name: "Alice" }), {}))
1916 * Option.map(result, (formData) => formData.get("name")) // => Option.some("Alice")
1917 * ```
1918 *
1919 * @see {@link decodeFormData} for the corresponding decoder
1920 * @see {@link collectBracketPathEntries} for the underlying flattener
1921 * @see {@link encodeURLSearchParams} for the URLSearchParams variant
1922 *
1923 * @category encoding
1924 * @since 4.0.0
1925 */
1926export function encodeFormData(): Getter<FormData, unknown> {
1927 return transform((input) => {
1928 const out = new FormData()
1929 if (typeof input === "object" && input !== null) {
1930 const entries = collectFormDataEntries(input)
1931 entries.forEach(([key, value]) => {
1932 out.append(key, value)
1933 })
1934 }
1935 return out
1936 })
1937}
1938
1939/**
1940 * Decodes a `URLSearchParams` object into a nested tree structure using bracket-path notation.
1941 *
1942 * **When to use**
1943 *
1944 * Use when you need a schema getter to parse query parameters from URLs into
1945 * structured objects.
1946 *
1947 * **Details**
1948 *
1949 * The getter is pure and never fails. It interprets bracket-path keys such as
1950 * `user[name]` and `items[0]` to build nested objects or arrays, and each leaf
1951 * value is a `string`.
1952 *
1953 * **Example** (Decoding URLSearchParams)
1954 *
1955 * ```ts import.meta.vitest
1956 * import { Effect, Option, SchemaGetter } from "effect"
1957 *
1958 * const decode = SchemaGetter.decodeURLSearchParams()
1959 * const params = new URLSearchParams("user[name]=Alice")
1960 * const result = Effect.runSync(SchemaGetter.run(decode, Option.some(params), {}))
1961 * result // => Option.some({ user: { name: "Alice" } })
1962 * ```
1963 *
1964 * @see {@link encodeURLSearchParams} for the corresponding encoder
1965 * @see {@link makeTreeRecord} for the underlying bracket-path parser
1966 * @see {@link decodeFormData} for the FormData variant
1967 *
1968 * @category decoding
1969 * @since 4.0.0
1970 */
1971export function decodeURLSearchParams(): Getter<Schema.TreeRecord<string>, URLSearchParams> {
1972 return transform((input) => makeTreeRecord(Array.from(input.entries())))
1973}
1974
1975const collectURLSearchParamsEntries = collectBracketPathEntries(Predicate.isString)
1976
1977/**
1978 * Encodes a nested object into a `URLSearchParams` instance using bracket-path notation.
1979 *
1980 * **When to use**
1981 *
1982 * Use when you need a schema getter to serialize structured data to query
1983 * parameters for URLs.
1984 *
1985 * **Details**
1986 *
1987 * The getter is pure and never fails. It flattens nested objects or arrays into
1988 * bracket-path keys. Non-object inputs produce an empty `URLSearchParams`.
1989 *
1990 * **Example** (Encoding to URLSearchParams)
1991 *
1992 * ```ts import.meta.vitest
1993 * import { Effect, Option, SchemaGetter } from "effect"
1994 *
1995 * const encode = SchemaGetter.encodeURLSearchParams()
1996 * const result = Effect.runSync(SchemaGetter.run(encode, Option.some({ name: "Alice" }), {}))
1997 * Option.map(result, (params) => params.toString()) // => Option.some("name=Alice")
1998 * ```
1999 *
2000 * @see {@link decodeURLSearchParams} for the corresponding decoder
2001 * @see {@link collectBracketPathEntries} for the underlying flattener
2002 * @see {@link encodeFormData} for the FormData variant
2003 *
2004 * @category encoding
2005 * @since 4.0.0
2006 */
2007export function encodeURLSearchParams(): Getter<URLSearchParams, unknown> {
2008 return transform((input) => {
2009 if (typeof input === "object" && input !== null) {
2010 return new URLSearchParams(collectURLSearchParamsEntries(input))
2011 }
2012 return new URLSearchParams()
2013 })
2014}
2015
2016function bracketPathToTokens(bracketPath: string): Array<string | number> {
2017 // real empty path (from append("", value))
2018 if (bracketPath === "") {
2019 return [""]
2020 }
2021
2022 const replaced = bracketPath.replace(/\[(.*?)\]/g, ".$1")
2023 const parts = replaced.split(".")
2024 // if bracket path started with "[...]" we get ".foo" => ["", "foo"]; drop the synthetic first ""
2025 const start = replaced.startsWith(".") ? 1 : 0
2026
2027 return parts
2028 .slice(start)
2029 .map((part) => (Arr.isCanonicalArrayIndex(part) ? globalThis.Number(part) : part))
2030}
2031
2032/**
2033 * Builds a nested tree object from a list of bracket-path entries.
2034 *
2035 * **When to use**
2036 *
2037 * Use when you need a schema getter to parse FormData or URLSearchParams
2038 * entries into structured objects.
2039 * - You have flat key-value pairs with bracket-path keys that need nesting.
2040 *
2041 * **Details**
2042 *
2043 * - A bracket path is a string like `"user[address][city]"` that describes nested
2044 * object/array structure.
2045 * - Interprets bracket paths and constructs the corresponding nested object.
2046 * - Builds and returns a nested object from the input entries.
2047 * - Supported syntax:
2048 * - `"foo"` → object key `"foo"`
2049 * - `"foo[bar]"` → nested `{ foo: { bar: ... } }`
2050 * - `"foo[0]"` → array index `{ foo: [value] }`
2051 * - `"foo[]"` → append to array `foo`
2052 * - `""` → real empty key
2053 * - Numeric bracket segments become array indices only when they are valid
2054 * JavaScript array-index strings; otherwise they remain object keys.
2055 * - Duplicate keys for the same path are merged into arrays.
2056 * - If a structural path conflicts with a previous leaf or a different container
2057 * type, the later structural path replaces the conflicting value.
2058 * - The notation has no escaping for `.`, `[` or `]`, so keys containing these
2059 * delimiters cannot be round-tripped without changing their structure.
2060 *
2061 * **Example** (Building a tree from bracket paths)
2062 *
2063 * ```ts import.meta.vitest
2064 * import { SchemaGetter } from "effect"
2065 *
2066 * SchemaGetter.makeTreeRecord([
2067 * ["user[name]", "Alice"],
2068 * ["user[tags][]", "admin"],
2069 * ["user[tags][]", "editor"]
2070 * ]) // => { user: { name: "Alice", tags: ["admin", "editor"] } }
2071 * ```
2072 *
2073 * @see {@link collectBracketPathEntries} for flattening trees into bracket-path entries
2074 * @see {@link decodeFormData} for a higher-level FormData decoder
2075 * @see {@link decodeURLSearchParams} for a higher-level URLSearchParams decoder
2076 *
2077 * @category constructors
2078 * @since 4.0.0
2079 */
2080export function makeTreeRecord<A>(
2081 bracketPathEntries: ReadonlyArray<readonly [bracketPath: string, value: A]>
2082): Schema.TreeRecord<A> {
2083 const out: any = {}
2084 const containers = new WeakSet<object>()
2085 const duplicates = new WeakSet<object>()
2086
2087 function getOrCreateContainer(self: any, key: PropertyKey, shouldBeArray: boolean): any {
2088 const current = Object.hasOwn(self, key) ? self[key] : undefined
2089 if (containers.has(current) && Array.isArray(current) === shouldBeArray) {
2090 return current
2091 }
2092 const container = shouldBeArray ? [] : {}
2093 containers.add(container)
2094 InternalRecord.assignProperty(self, key, container)
2095 return container
2096 }
2097
2098 bracketPathEntries.forEach(([key, value]) => {
2099 const tokens = bracketPathToTokens(key)
2100 let cur: any = out
2101 tokens.forEach((token, i) => {
2102 const isLast = i === tokens.length - 1
2103
2104 // We are inside an array and see "[]" (empty token) => append
2105 if (Array.isArray(cur) && token === "") {
2106 if (isLast) {
2107 cur.push(value)
2108 } else {
2109 // bracket path: "foo[][bar]" => push a new element and descend into it
2110 const next = tokens[i + 1]
2111 const shouldBeArray = typeof next === "number" || next === ""
2112 const index = cur.length
2113 cur = getOrCreateContainer(cur, index, shouldBeArray)
2114 }
2115 } else if (isLast) {
2116 // If we're setting a value at a path that already exists
2117 // convert it to an array to support multiple values for the same key
2118 const hasOwn = Object.hasOwn(cur, token)
2119 if (hasOwn && Array.isArray(cur[token]) && (containers.has(cur[token]) || duplicates.has(cur[token]))) {
2120 cur[token].push(value)
2121 } else if (hasOwn) {
2122 const values = [cur[token], value]
2123 duplicates.add(values)
2124 InternalRecord.assignProperty(cur, token, values)
2125 } else {
2126 InternalRecord.assignProperty(cur, token, value)
2127 }
2128 } else {
2129 const next = tokens[i + 1]
2130 // if next is a number OR "" (from []), we are building an array
2131 const shouldBeArray = typeof next === "number" || next === ""
2132 cur = getOrCreateContainer(cur, token, shouldBeArray)
2133 }
2134 })
2135 })
2136 return out
2137}
2138
2139/**
2140 * Flattens a nested object into bracket-path entries, filtering leaf values by a type guard.
2141 *
2142 * **When to use**
2143 *
2144 * Use when you need a schema getter to serialize structured objects to flat
2145 * key-value entries.
2146 * - Building custom `FormData` or `URLSearchParams` encoders.
2147 *
2148 * **Details**
2149 *
2150 * - Takes a nested object and produces flat `[bracketPath, value]` pairs suitable for
2151 * `FormData` or `URLSearchParams`.
2152 * - Returns a curried function: first call provides the leaf type guard, second call provides the object.
2153 * - Recursively traverses objects and arrays.
2154 * - If all elements of an array are leaves, encodes them as multiple entries with the same key
2155 * (e.g. `tags=a&tags=b`). Otherwise uses indexed bracket paths (e.g. `items[0]`, `items[1]`).
2156 * - Non-leaf values that aren't objects or arrays are silently skipped.
2157 * - Empty arrays and objects produce no entries, and path delimiters in property
2158 * names are not escaped. The resulting format is therefore lossy.
2159 *
2160 * **Example** (Flattening an object to bracket paths)
2161 *
2162 * ```ts import.meta.vitest
2163 * import { Predicate, SchemaGetter } from "effect"
2164 *
2165 * const collectStrings = SchemaGetter.collectBracketPathEntries(Predicate.isString)
2166 * const entries = collectStrings({ user: { name: "Alice", tags: ["admin", "editor"] } })
2167 *
2168 * entries // => [["user[name]", "Alice"], ["user[tags]", "admin"], ["user[tags]", "editor"]]
2169 * ```
2170 *
2171 * @see {@link makeTreeRecord} for building trees from bracket-path entries
2172 * @see {@link encodeFormData} for a higher-level FormData encoder
2173 * @see {@link encodeURLSearchParams} for a higher-level URLSearchParams encoder
2174 *
2175 * @category converting
2176 * @since 4.0.0
2177 */
2178export function collectBracketPathEntries<A>(isLeaf: (value: unknown) => value is A) {
2179 return (input: object): Array<[bracketPath: string, value: A]> => {
2180 const bracketPathEntries: Array<[string, A]> = []
2181
2182 function append(key: string, value: unknown): void {
2183 if (isLeaf(value)) {
2184 bracketPathEntries.push([key, value])
2185 } else if (Array.isArray(value)) {
2186 // If all values are leaves, encode as multiple entries with the same key
2187 const allLeaves = value.every(isLeaf)
2188 if (allLeaves) {
2189 value.forEach((v) => {
2190 bracketPathEntries.push([key, v])
2191 })
2192 } else {
2193 value.forEach((v, i) => {
2194 append(`${key}[${i}]`, v)
2195 })
2196 }
2197 } else if (typeof value === "object" && value !== null) {
2198 for (const [k, v] of Object.entries(value)) {
2199 append(`${key}[${k}]`, v)
2200 }
2201 }
2202 }
2203
2204 for (const [key, value] of Object.entries(input)) {
2205 append(key, value)
2206 }
2207
2208 return bracketPathEntries
2209 }
2210}