1/**
2 * Descriptions of configuration values that can be read from a
3 * `ConfigProvider`. A `Config<T>` explains which keys to read, how to decode
4 * and validate them, and how to combine defaults, fallbacks, nested paths, and
5 * multiple settings. Configs are also Effects, so they can be yielded in
6 * `Effect.gen` after a provider has been supplied.
7 *
8 * @since 4.0.0
9 */
10import type { Path, SourceError } from "./ConfigProvider.ts"
11import * as ConfigProvider from "./ConfigProvider.ts"
12import * as Effect from "./Effect.ts"
13import * as Effectable from "./Effectable.ts"
14import { dual, memoize } from "./Function.ts"
15import * as InternalRecord from "./internal/record.ts"
16import * as LogLevel_ from "./LogLevel.ts"
17import * as Option from "./Option.ts"
18import * as Predicate from "./Predicate.ts"
19import * as Rec from "./Record.ts"
20import * as Result from "./Result.ts"
21import * as Schema from "./Schema.ts"
22import * as SchemaAST from "./SchemaAST.ts"
23import * as SchemaGetter from "./SchemaGetter.ts"
24import * as SchemaIssue from "./SchemaIssue.ts"
25import * as SchemaParser from "./SchemaParser.ts"
26import * as SchemaTransformation from "./SchemaTransformation.ts"
27
28const TypeId = "~effect/Config"
29
30/**
31 * Returns `true` if `u` is a `Config` instance.
32 *
33 * **When to use**
34 *
35 * Use when you need to distinguish a `Config` from an unknown value before
36 * calling `.parse` or {@link unwrap}.
37 *
38 * **Example** (Checking Config values)
39 *
40 * ```ts import.meta.vitest
41 * import { Config } from "effect"
42 *
43 * Config.isConfig(Config.String("HOST")) // => true
44 * Config.isConfig("not a config") // => false
45 * ```
46 *
47 * @category guards
48 * @since 2.0.0
49 */
50export const isConfig = (u: unknown): u is Config<unknown> => Predicate.hasProperty(u, TypeId)
51
52/**
53 * Represents the error type produced when config loading or validation fails.
54 *
55 * **When to use**
56 *
57 * Use when you need to inspect config loading or validation failures.
58 *
59 * **Details**
60 *
61 * Wraps either:
62 * - A `SourceError` — the provider could not read data (I/O failure).
63 * - A `SchemaError` — the data was found but did not match the schema
64 * (wrong type, out of range, missing key, etc.).
65 *
66 * @see {@link orElse} – recover from a ConfigError
67 * @see {@link withDefault} – provide a fallback when relevant input is absent
68 *
69 * @category errors
70 * @since 4.0.0
71 */
72export class ConfigError {
73 readonly _tag = "ConfigError"
74 readonly name: string = "ConfigError"
75 readonly cause: SourceError | Schema.SchemaError
76 constructor(cause: SourceError | Schema.SchemaError) {
77 this.cause = cause
78 }
79 get message() {
80 return this.cause.toString()
81 }
82 toString() {
83 return `ConfigError(${this.message})`
84 }
85}
86
87/**
88 * A recipe for extracting a typed value `T` from a `ConfigProvider`.
89 *
90 * **When to use**
91 *
92 * Use to describe typed configuration that can be parsed from a provider or
93 * yielded inside `Effect.gen`.
94 *
95 * **Details**
96 *
97 * Key members:
98 * - `parse(provider)` – runs the config against a specific provider.
99 * - Yieldable – can be yielded inside `Effect.gen`, which automatically
100 * resolves the current `ConfigProvider` from the context.
101 * - Pipeable – supports `.pipe(Config.map(...))` etc.
102 *
103 * @see {@link schema} – the main way to create a Config
104 *
105 * @category models
106 * @since 2.0.0
107 */
108export interface Config<out T> extends Effect.Effect<T, ConfigError> {
109 readonly [TypeId]: typeof TypeId
110 readonly parse: (provider: ConfigProvider.ConfigProvider) => Effect.Effect<T, ConfigError>
111}
112
113// Result failure represents recoverable absence; Effect failure represents an
114// invalid value or a source error. Keep them separate until parsing completes.
115type Resolution<T> = Result.Result<T, ConfigError>
116
117type Evaluator<T> = (
118 provider: ConfigProvider.ConfigProvider,
119 pathPrefix: Path
120) => Effect.Effect<Resolution<T>, ConfigError>
121
122interface ConfigImpl<out T> extends Config<T> {
123 readonly evaluator: Evaluator<T>
124}
125
126const Proto = {
127 ...Effectable.Prototype<Config<any>>({
128 label: "Config",
129 evaluate(fiber) {
130 return this.parse(fiber.getRef(ConfigProvider.ConfigProvider))
131 }
132 }),
133 [TypeId]: TypeId,
134 toJSON(this: Config<unknown>) {
135 return {
136 _id: "Config"
137 }
138 }
139}
140
141function make<T>(
142 evaluator: Evaluator<T>
143): Config<T> {
144 const self = Object.create(Proto)
145 self.evaluator = evaluator
146 self.parse = (provider: ConfigProvider.ConfigProvider) =>
147 Effect.flatMapEager(evaluator(provider, []), Effect.fromResult)
148 return self
149}
150
151const evaluateAt = <T>(
152 self: Config<T>,
153 provider: ConfigProvider.ConfigProvider,
154 pathPrefix: Path
155): Effect.Effect<Resolution<T>, ConfigError> => (self as ConfigImpl<T>).evaluator(provider, pathPrefix)
156
157/**
158 * Transforms the parsed value of a config with a pure function.
159 *
160 * **When to use**
161 *
162 * Use when you need to transform a parsed config value with a function that
163 * cannot fail.
164 *
165 * **Example** (Uppercasing a string config)
166 *
167 * ```ts import.meta.vitest
168 * import { Config, ConfigProvider, Effect } from "effect"
169 *
170 * const upper = Config.String("name").pipe(
171 * Config.map((s) => s.toUpperCase())
172 * )
173 *
174 * const provider = ConfigProvider.fromUnknown({ name: "alice" })
175 * Effect.runSync(upper.parse(provider)) // => "ALICE"
176 * ```
177 *
178 * @see {@link mapEffect} – when the transformation can fail
179 *
180 * @category mapping
181 * @since 2.0.0
182 */
183export const map: {
184 <A, B>(f: (a: A) => B): (self: Config<A>) => Config<B>
185 <A, B>(self: Config<A>, f: (a: A) => B): Config<B>
186} = dual(2, <A, B>(self: Config<A>, f: (a: A) => B): Config<B> => {
187 return make((provider, pathPrefix) => Effect.map(evaluateAt(self, provider, pathPrefix), Result.map(f)))
188})
189
190/**
191 * Lets you sequence multiple configs that depend on each other or branch to
192 * multiple different configs depending on the parent.
193 *
194 * **When to use**
195 *
196 * Use when you want to maintain 2 sets of env vars with different names, and switch
197 * between whole sets based on a single dedicated env var. Or when you want to
198 * provide a variety of environment variable names (`PORT`, `BACKEND_PORT`,
199 * `API_PORT`) in order of preference for a specific configuration option, such
200 * as the listening port.
201 *
202 * **Example** (fallback configs and branching)
203 *
204 * ```ts import.meta.vitest
205 * import { Config, Option } from "effect"
206 *
207 * const EnvVar = (name: string) =>
208 * Config.Literals([
209 * ...["prod", "production", "PROD", "PRODUCTION"] as const,
210 * ...["dev", "development", "DEV", "DEVELOPMENT"] as const
211 * ], name)
212 *
213 * const withAbsenceFallback = <A, B>(fallback: Config.Config<A>) => (self: Config.Config<B>): Config.Config<A | B> =>
214 * Config.flatMap(
215 * Config.option(self),
216 * Option.match({
217 * onNone: () => fallback,
218 * onSome: Config.succeed<A | B>
219 * })
220 * )
221 *
222 * // const ENV: Config.Config<"dev" | "prod">
223 * const ENV = EnvVar("ENV").pipe(
224 * withAbsenceFallback(EnvVar("NODE_ENV")),
225 * Config.map((fuzzyEnv) =>
226 * fuzzyEnv.toLowerCase().startsWith("dev") ? "dev" : "prod"),
227 * Config.withDefault('dev')
228 * )
229 *
230 * // const hostConfig: Config.Config<string>
231 * const hostConfig = ENV.pipe(
232 * Config.flatMap((env) =>
233 * env === "dev"
234 * // dev is very forgiving
235 * ? Config.NonEmptyString("DEV_HOST").pipe(
236 * Config.orElse(() => Config.NonEmptyString("HOST")),
237 * Config.orElse(() => Config.succeed("localhost"))
238 * )
239 * // prod is much stricter
240 * : Config.NonEmptyString("PROD_HOST").pipe(
241 * withAbsenceFallback(Config.NonEmptyString("HOST"))
242 * )
243 * )
244 * )
245 * ```
246 *
247 * @category mapping
248 * @since 4.0.0
249 */
250export const flatMap: {
251 <A, B>(f: (a: A) => Config<B>): (self: Config<A>) => Config<B>
252 <A, B>(self: Config<A>, f: (a: A) => Config<B>): Config<B>
253} = dual(2, <A, B>(self: Config<A>, f: (a: A) => Config<B>): Config<B> => {
254 return make((provider, pathPrefix) =>
255 Effect.flatMap(
256 evaluateAt(self, provider, pathPrefix),
257 Result.match({
258 onSuccess: (success) => evaluateAt(f(success), provider, pathPrefix),
259 onFailure: (error) => Effect.succeed(Result.fail(error))
260 })
261 )
262 )
263})
264
265/**
266 * Transforms the parsed value with a function that may fail.
267 *
268 * **When to use**
269 *
270 * Use when you need to transform a parsed config value with a function that can
271 * produce a `ConfigError` (e.g. parsing a URL, checking a range).
272 *
273 * **Example** (Wrapping a value in an effectful transformation)
274 *
275 * ```ts import.meta.vitest
276 * import { Config, ConfigProvider, Effect } from "effect"
277 *
278 * const trimmed = Config.String("name").pipe(
279 * Config.mapEffect((s) => Effect.succeed(s.trim()))
280 * )
281 * const provider = ConfigProvider.fromUnknown({ name: " Alice " })
282 * Effect.runSync(trimmed.parse(provider)) // => "Alice"
283 * ```
284 *
285 * @see {@link map} – when the transformation cannot fail
286 *
287 * @category mapping
288 * @since 2.0.0
289 */
290export const mapEffect: {
291 <A, B>(f: (a: A) => Effect.Effect<B, ConfigError>): (self: Config<A>) => Config<B>
292 <A, B>(self: Config<A>, f: (a: A) => Effect.Effect<B, ConfigError>): Config<B>
293} = dual(2, <A, B>(self: Config<A>, f: (a: A) => Effect.Effect<B, ConfigError>): Config<B> => {
294 return make((provider, pathPrefix) =>
295 Effect.flatMap(
296 evaluateAt(self, provider, pathPrefix),
297 (resolution): Effect.Effect<Resolution<B>, ConfigError> =>
298 Result.isSuccess(resolution)
299 ? Effect.mapEager(f(resolution.success), Result.succeed)
300 : Effect.succeed(Result.fail(resolution.failure))
301 )
302 )
303})
304
305/**
306 * Provides a fallback config when parsing fails with a `ConfigError`.
307 *
308 * **When to use**
309 *
310 * Use when you need to try an alternative config source after the primary one
311 * fails.
312 *
313 * **Details**
314 *
315 * Unlike {@link withDefault}, this handles both semantic absence and **all**
316 * `ConfigError`s. The fallback function receives the error and returns a new
317 * `Config`.
318 *
319 * **Gotchas**
320 *
321 * The fallback's result replaces the original failure. If the fallback is
322 * absent, an outer {@link withDefault} or {@link option} can recover it. If the
323 * fallback fails, only its error propagates.
324 *
325 * **Example** (Trying another port before using a default)
326 *
327 * ```ts import.meta.vitest
328 * import { Config, ConfigProvider, Effect } from "effect"
329 *
330 * const port = Config.Int("PORT").pipe(
331 * Config.orElse(() => Config.Int("BACKUP_PORT")),
332 * Config.withDefault(3000)
333 * )
334 * const provider = ConfigProvider.fromUnknown({ PORT: "invalid", BACKUP_PORT: "8080" })
335 * await Effect.runPromise(port.parse(provider)) // => 8080
336 *
337 * const missingBackup = ConfigProvider.fromUnknown({ PORT: "invalid" })
338 * await Effect.runPromise(port.parse(missingBackup)) // => 3000
339 * ```
340 *
341 * @see {@link withDefault} – fallback only on semantic absence
342 *
343 * @category combinators
344 * @since 2.0.0
345 */
346export const orElse: {
347 <A2>(that: (error: ConfigError) => Config<A2>): <A>(self: Config<A>) => Config<A2 | A>
348 <A, A2>(self: Config<A>, that: (error: ConfigError) => Config<A2>): Config<A | A2>
349} = dual(2, <A, A2>(self: Config<A>, that: (error: ConfigError) => Config<A2>): Config<A | A2> => {
350 return make<A | A2>((provider, pathPrefix) =>
351 Effect.matchEffect(evaluateAt(self, provider, pathPrefix), {
352 onFailure: (error) => evaluateAt(that(error), provider, pathPrefix),
353 onSuccess: (resolution): Effect.Effect<Resolution<A | A2>, ConfigError> =>
354 Result.isFailure(resolution)
355 ? evaluateAt(that(resolution.failure), provider, pathPrefix)
356 : Effect.succeed(resolution)
357 })
358 )
359})
360
361/**
362 * Combines multiple configs into a single config that parses all of them.
363 *
364 * **When to use**
365 *
366 * Use when you need to group related configs into a tuple or named struct.
367 *
368 * **Details**
369 *
370 * Accepts a tuple (preserves positions), an iterable, or a record of configs.
371 * Returns a config whose parsed value mirrors the input shape.
372 *
373 * A combined config is absent when any child is absent and no child fails.
374 * Validation and source errors propagate even when another child is absent.
375 * An outer {@link withDefault} replaces the entire absent group, while
376 * {@link option} returns `None`. Apply defaults to individual children to
377 * preserve the values of other children.
378 *
379 * Unlike a `Schema.Struct` passed to {@link schema}, `all` combines independent
380 * lookups. A struct validates an existing object and fails when required fields
381 * are missing; `all` can recover missing children through a group default.
382 *
383 * **Example** (Defaulting an incomplete config group)
384 *
385 * ```ts import.meta.vitest
386 * import { Config, ConfigProvider, Effect } from "effect"
387 *
388 * const dbConfig = Config.all({
389 * host: Config.String("host"),
390 * port: Config.Number("port")
391 * }).pipe(Config.withDefault({ host: "localhost", port: 5432 }))
392 *
393 * const provider = ConfigProvider.fromUnknown({ host: "db.internal", port: 6000 })
394 * await Effect.runPromise(dbConfig.parse(provider)) // => { host: "db.internal", port: 6000 }
395 *
396 * const missingPort = ConfigProvider.fromUnknown({ host: "db.internal" })
397 * await Effect.runPromise(dbConfig.parse(missingPort)) // => { host: "localhost", port: 5432 }
398 * ```
399 *
400 * @category combinators
401 * @since 2.0.0
402 */
403export function all<const Arg extends Iterable<Config<any>> | Record<string, Config<any>>>(
404 arg: Arg
405): Config<
406 [Arg] extends [ReadonlyArray<Config<any>>] ? {
407 -readonly [K in keyof Arg]: [Arg[K]] extends [Config<infer A>] ? A : never
408 }
409 : [Arg] extends [Iterable<Config<infer A>>] ? Array<A>
410 : [Arg] extends [Record<string, Config<any>>] ? {
411 -readonly [K in keyof Arg]: [Arg[K]] extends [Config<infer A>] ? A : never
412 }
413 : never
414> {
415 const configs: Array<Config<any>> | Record<string, Config<any>> = globalThis.Array.isArray(arg)
416 ? arg
417 : Symbol.iterator in arg
418 ? [...arg as any]
419 : arg
420 return make<any>((provider, pathPrefix) =>
421 globalThis.Array.isArray(configs)
422 ? Effect.mapEager(
423 Effect.all(configs.map((config) => evaluateAt(config, provider, pathPrefix))),
424 Result.all
425 )
426 : Effect.mapEager(
427 Effect.all(Rec.map(configs, (config) => evaluateAt(config, provider, pathPrefix))),
428 Result.all
429 )
430 ) as any
431}
432
433/**
434 * Provides a fallback value when the config is absent.
435 *
436 * **When to use**
437 *
438 * Use when you need to make a config key optional with a sensible default.
439 *
440 * **Gotchas**
441 *
442 * Validation and source errors still propagate. For an {@link all} group, any
443 * absent child causes the default to replace the entire group unless another
444 * child fails. Apply defaults to individual children to preserve other values.
445 *
446 * A schema that successfully decodes absent input keeps its decoded value
447 * instead of using the default. Schema configs first represent a missing or
448 * incompatible provider shape as `undefined`; the default is used only when
449 * the schema rejects that value and no relevant input was found.
450 *
451 * **Example** (Defaulting a missing port)
452 *
453 * ```ts import.meta.vitest
454 * import { Config, ConfigProvider, Effect } from "effect"
455 *
456 * const port = Config.Number("port").pipe(Config.withDefault(3000))
457 *
458 * const provider = ConfigProvider.fromUnknown({})
459 * Effect.runSync(port.parse(provider)) // => 3000
460 * ```
461 *
462 * @see {@link option} – returns `Option` instead of a default value
463 * @see {@link orElse} – catches all errors, not just absent input
464 *
465 * @category combinators
466 * @since 2.0.0
467 */
468export const withDefault: {
469 <const A2>(defaultValue: A2): <A>(self: Config<A>) => Config<A2 | A>
470 <A, const A2>(self: Config<A>, defaultValue: A2): Config<A | A2>
471} = dual(2, <A, const A2>(self: Config<A>, defaultValue: A2): Config<A | A2> => {
472 return make<A | A2>((provider, pathPrefix) =>
473 Effect.mapEager(
474 evaluateAt(self, provider, pathPrefix),
475 (resolution) => Result.isFailure(resolution) ? Result.succeed(defaultValue) : resolution
476 )
477 )
478})
479
480/**
481 * Makes a config optional: returns `Some(value)` on success and `None` when the
482 * config is absent.
483 *
484 * **When to use**
485 *
486 * Use when you need to handle a config key that may or may not be present.
487 *
488 * **Gotchas**
489 *
490 * Validation and source errors still propagate. For an {@link all} group, any
491 * absent child produces `None` unless another child fails. Successful values
492 * are always wrapped in `Some`, including `undefined` when the schema
493 * explicitly accepts it. Schema configs first represent a missing or
494 * incompatible provider shape as `undefined`; `None` is returned only when the
495 * schema rejects that value and no relevant input was found.
496 *
497 * **Example** (Reading optional config)
498 *
499 * ```ts import.meta.vitest
500 * import { Config, ConfigProvider, Effect, Option } from "effect"
501 *
502 * const maybePort = Config.option(Config.Number("port"))
503 *
504 * const provider = ConfigProvider.fromUnknown({})
505 * Effect.runSync(maybePort.parse(provider)) // => Option.none()
506 * ```
507 *
508 * @see {@link withDefault} – provide a concrete fallback value instead
509 *
510 * @category combinators
511 * @since 2.0.0
512 */
513export const option = <A>(self: Config<A>): Config<Option.Option<A>> =>
514 self.pipe(map(Option.some), withDefault(Option.none()))
515
516/**
517 * Extracts the successfully parsed value type from a `Config`.
518 *
519 * **When to use**
520 *
521 * Use to derive the parsed value type from an existing `Config` value when
522 * declaring reusable config-driven types.
523 *
524 * @see {@link Config} for the config type whose parsed value is extracted
525 * @see {@link Effect.Success} for extracting the success type from any `Effect`
526 *
527 * @category utility types
528 * @since 2.5.0
529 */
530export type Success<T> = [T] extends [Config<infer A>] ? A : never
531
532/**
533 * Utility type that recursively replaces primitives with `Config` in a nested
534 * structure.
535 *
536 * **When to use**
537 *
538 * Use when typing the input of {@link unwrap} so callers can pass either a `Config`
539 * or a record of `Config`s.
540 *
541 * **Details**
542 *
543 * `Config.Wrap<{ key: string }>` becomes `{ key: Config<string> } | Config<{ key: string }>`
544 *
545 * @see {@link unwrap} – construct a `Config` from a `Wrap<T>`
546 *
547 * @category utility types
548 * @since 2.0.0
549 */
550export type Wrap<A> = [NonNullable<A>] extends [infer T] ? [IsPlainObject<T>] extends [true] ?
551 | { readonly [K in keyof A]: Wrap<A[K]> }
552 | Config<A>
553 : Config<A>
554 : Config<A>
555
556type IsPlainObject<A> = [A] extends [Record<string, any>]
557 ? [keyof A] extends [never] ? false : [keyof A] extends [string] ? true : false
558 : false
559
560/**
561 * Constructs a `Config<T>` from a value matching `Wrap<T>`.
562 *
563 * **When to use**
564 *
565 * Use when accepting config from callers who may pass either a single `Config` or a
566 * record of individual `Config`s.
567 *
568 * **Details**
569 *
570 * If the input is already a `Config`, it is returned as-is. Otherwise, each
571 * key is recursively unwrapped and combined.
572 *
573 * **Example** (Unwrapping a record of configs)
574 *
575 * ```ts import.meta.vitest
576 * import { Config, ConfigProvider, Effect } from "effect"
577 *
578 * interface Options {
579 * key: string
580 * }
581 *
582 * const makeConfig = (config: Config.Wrap<Options>): Config.Config<Options> =>
583 * Config.unwrap(config)
584 *
585 * const config = makeConfig({ key: Config.String("key") })
586 * const provider = ConfigProvider.fromUnknown({ key: "value" })
587 * Effect.runSync(config.parse(provider)) // => { key: "value" }
588 * ```
589 *
590 * @see {@link Wrap} – the utility type accepted by this function
591 *
592 * @category converting
593 * @since 2.0.0
594 */
595export const unwrap = <T>(wrapped: Wrap<T>): Config<T> => {
596 if (isConfig(wrapped)) return wrapped
597 return all(Rec.map(wrapped as Record<string, Wrap<any>>, (config) => unwrap(config))) as Config<T>
598}
599
600// -----------------------------------------------------------------------------
601// schema
602// -----------------------------------------------------------------------------
603
604interface ConfigCursor {
605 readonly provider: ConfigProvider.ConfigProvider
606 readonly path: Path
607 readonly node: ConfigProvider.Node | undefined
608 readonly toString: () => string
609}
610
611const isSourceError = (u: unknown): u is ConfigProvider.SourceError => Predicate.isTagged(u, "SourceError")
612
613const cursorToString = (): string => "<configuration>"
614
615const loadCursor: (
616 provider: ConfigProvider.ConfigProvider,
617 path: Path
618) => Effect.Effect<ConfigCursor> = (provider, path) =>
619 provider.load(path).pipe(
620 Effect.orDie,
621 Effect.mapEager((node) => ({ provider, path, node, toString: cursorToString }))
622 )
623
624const loadChildCursor = (cursor: ConfigCursor, segment: string | number): Effect.Effect<ConfigCursor> =>
625 loadCursor(cursor.provider, [...cursor.path, segment])
626
627const decodeFromCursor = (
628 ast: SchemaAST.AST,
629 decode: (cursor: ConfigCursor) => Effect.Effect<unknown, SchemaIssue.Issue>
630): SchemaAST.AST =>
631 SchemaAST.decodeTo(
632 SchemaAST.unknown,
633 ast,
634 new SchemaTransformation.Transformation(
635 SchemaGetter.transformEffect((input: unknown) => decode(input as ConfigCursor)),
636 SchemaGetter.passthrough()
637 )
638 )
639
640const isScalarInput = (ast: SchemaAST.AST): boolean => {
641 switch (ast._tag) {
642 case "Union":
643 return ast.types.every(isScalarInput)
644 case "Objects":
645 case "Arrays":
646 case "Suspend":
647 return false
648 default:
649 return true
650 }
651}
652
653const hasProviderInput = (
654 ast: SchemaAST.AST,
655 node: ConfigProvider.Node | undefined
656): boolean => {
657 switch (ast._tag) {
658 case "Objects":
659 return node?._tag === "Record"
660 case "Arrays":
661 return node?._tag === "Array"
662 case "Union":
663 return ast.types.some((ast) => hasProviderInput(ast, node))
664 case "Suspend":
665 return hasProviderInput(ast.thunk(), node)
666 default:
667 return node?.value !== undefined
668 }
669}
670
671const toConfigCursorAST = memoize((root: SchemaAST.AST): SchemaAST.AST => {
672 const seen = new WeakSet<SchemaAST.AST>()
673 const recur = SchemaAST.applyToSelfOrLastLinkEncoding((ast) => {
674 seen.add(ast)
675 switch (ast._tag) {
676 case "Objects": {
677 const matchesIndex = ast.indexSignatures.map((is) => SchemaParser._is(is.parameter))
678 const materialize = Effect.fnUntraced(function*(cursor: ConfigCursor) {
679 if (cursor.node?._tag !== "Record") {
680 return undefined
681 }
682 const node = cursor.node
683 const keys = new Set<string>()
684 for (const property of ast.propertySignatures) {
685 if (typeof property.name === "string") keys.add(property.name)
686 }
687 if (matchesIndex.length > 0) {
688 for (const key of node.keys) {
689 if (matchesIndex.some((matches) => matches(key))) keys.add(key)
690 }
691 }
692 const out: Record<string, ConfigCursor> = {}
693 for (const key of keys) {
694 const child = yield* loadChildCursor(cursor, key)
695 if (child.node !== undefined) InternalRecord.assignProperty(out, key, child)
696 }
697 return out
698 })
699 return decodeFromCursor(ast.recur(recur, (ast) => ast), materialize)
700 }
701 case "Arrays": {
702 const materialize = Effect.fnUntraced(function*(cursor: ConfigCursor) {
703 if (cursor.node?._tag !== "Array") {
704 return undefined
705 }
706 const out: Array<ConfigCursor> = []
707 for (let i = 0; i < cursor.node.length; i++) {
708 out.push(yield* loadChildCursor(cursor, i))
709 }
710 return out
711 })
712 return decodeFromCursor(ast.recur(recur), materialize)
713 }
714 case "Union":
715 for (const member of ast.types) {
716 recur(member)
717 }
718 if (!isScalarInput(ast)) return ast.recur(recur)
719 break
720 case "Suspend": {
721 const target = ast.thunk()
722 // Force new branches so opaque encodings fail when the Config is constructed.
723 if (!seen.has(target)) recur(target)
724 return ast.recur(recur)
725 }
726 case "Declaration":
727 case "Any":
728 throw new globalThis.Error("Config.schema does not support opaque StringTree encodings", { cause: ast })
729 }
730 return decodeFromCursor(ast, (cursor) => Effect.succeed(cursor.node?.value))
731 })
732 return recur(root)
733})
734
735/**
736 * Creates a `Config<T>` from a `Schema.Codec`.
737 *
738 * **When to use**
739 *
740 * Use when you need to read structured or schema-validated configuration.
741 *
742 * **Details**
743 *
744 * The optional `path` sets the local path segment(s) for the config lookup.
745 * It is appended to the logical path prefix accumulated from outer
746 * {@link nested} calls. Pass a single string for a flat key or an array for
747 * nested paths.
748 *
749 * Convenience constructors such as `String`, `Number`, and `Boolean` delegate
750 * to this API.
751 *
752 * The codec is converted to its canonical `StringTree` form. Its encoded shape
753 * determines how provider data is loaded: scalar schemas read a co-located
754 * scalar value, object schemas read declared properties and matching record
755 * keys, and array schemas read indexed children. A mixed-shape union loads each
756 * member according to that member's shape before applying the union's mode and
757 * checks.
758 *
759 * At the config's lookup path, a missing node or a node that cannot provide the
760 * representation required by the schema is decoded as `undefined`. Missing
761 * object properties remain omitted so the schema's property semantics still
762 * apply. Decoding success always wins, even when no provider input was found.
763 * For example,
764 * `Schema.UndefinedOr(Schema.String)` decodes to `undefined` and is not replaced
765 * by {@link withDefault}. If decoding fails and no relevant representation was
766 * found, the config is absent. Invalid data in a relevant representation is a
767 * validation failure. Provider `SourceError`s are always failures.
768 *
769 * **Gotchas**
770 *
771 * Plain `Schema.Array` and `Schema.Record` schemas use structural provider
772 * input. Use {@link Array} or {@link Record} when a flat separated string must
773 * also be accepted.
774 *
775 * `Schema.Struct` and {@link all} describe different lookup models. An
776 * existing object, even an empty one, is validated as a whole by a struct;
777 * missing required fields cause a validation failure. An `all` group combines
778 * independent lookups and is absent if any child is absent and none fail.
779 *
780 * The canonical `StringTree` encoding must expose a concrete scalar, object,
781 * array, or union shape. Opaque encodings such as `Schema.Any`,
782 * `Schema.Unknown`, `Schema.ObjectKeyword`, `Schema.Json`, and
783 * `Schema.MutableJson` are rejected synchronously when this config is
784 * constructed, including when they are nested in another schema. Suspended
785 * recursive schemas remain supported when their eventual shape is concrete.
786 * Declarations such as `Schema.URL` also remain supported when their canonical
787 * encoding has a concrete shape. To read arbitrary JSON from one scalar value,
788 * use `Schema.fromJsonString(Schema.Json)`.
789 *
790 * **Example** (Reading a structured config)
791 *
792 * ```ts import.meta.vitest
793 * import { Config, ConfigProvider, Effect, Schema } from "effect"
794 *
795 * const DbConfig = Config.schema(
796 * Schema.Struct({
797 * host: Schema.String,
798 * port: Schema.Int
799 * }),
800 * "db"
801 * )
802 *
803 * const provider = ConfigProvider.fromUnknown({
804 * db: { host: "localhost", port: 5432 }
805 * })
806 *
807 * Effect.runSync(DbConfig.parse(provider)) // => { host: "localhost", port: 5432 }
808 * ```
809 *
810 * @see {@link String} / {@link Number} / {@link Boolean} – shortcuts for
811 * single-value configs
812 *
813 * @category schemas
814 * @since 4.0.0
815 */
816export function schema<T>(codec: Schema.ConstraintCodec<T, unknown>, path?: string | ConfigProvider.Path): Config<T> {
817 const codecStringTree = Schema.toCodecStringTree(codec)
818 const encodedAst = SchemaAST.toEncoded(codecStringTree.ast)
819 const decodeCursor = SchemaParser.decodeUnknownEffect(
820 Schema.make<Schema.Codec<T, ConfigCursor>>(toConfigCursorAST(codecStringTree.ast))
821 )
822 const localPath = typeof path === "string" ? [path] : path ?? []
823 return make((provider, pathPrefix) => {
824 const fullPath = [...pathPrefix, ...localPath]
825 return loadCursor(provider, fullPath).pipe(
826 Effect.flatMapEager((cursor) =>
827 decodeCursor(cursor).pipe(
828 Effect.mapEager(Result.succeed),
829 Effect.catchEager((issue) => {
830 const error = new ConfigError(
831 new Schema.SchemaError(fullPath.length > 0 ? new SchemaIssue.Pointer(fullPath, issue) : issue)
832 )
833 return hasProviderInput(encodedAst, cursor.node)
834 ? Effect.fail(error)
835 : Effect.succeed(Result.fail(error))
836 })
837 )
838 ),
839 Effect.catchDefect((defect) => isSourceError(defect) ? Effect.fail(new ConfigError(defect)) : Effect.die(defect))
840 )
841 })
842}
843
844const PortSchema = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 }))
845
846const LogLevelSchema = Schema.Literals(LogLevel_.values)
847
848interface ArrayOptions {
849 readonly separator?: string | undefined
850}
851
852interface RecordOptions {
853 readonly separator?: string | undefined
854 readonly keyValueSeparator?: string | undefined
855}
856
857const isPath = (u: unknown): u is string | Path => Predicate.isString(u) || globalThis.Array.isArray(u)
858
859// -----------------------------------------------------------------------------
860// constructors
861// -----------------------------------------------------------------------------
862
863/**
864 * Creates a config that always fails with the given error.
865 *
866 * **When to use**
867 *
868 * Use when you need to re-raise a specific config error, such as inside
869 * {@link orElse}.
870 *
871 * @category constructors
872 * @since 2.0.0
873 */
874export function fail(err: SourceError | Schema.SchemaError): Config<never> {
875 return make(() => Effect.fail(new ConfigError(err)))
876}
877
878/**
879 * Creates a config that always succeeds with the given value, ignoring the
880 * provider entirely.
881 *
882 * **When to use**
883 *
884 * Use when you need a hardcoded config value, such as inside {@link orElse} or
885 * tests.
886 *
887 * **Example** (Returning a constant fallback)
888 *
889 * ```ts import.meta.vitest
890 * import { Config, ConfigProvider, Effect } from "effect"
891 *
892 * const host = Config.String("HOST").pipe(
893 * Config.orElse(() => Config.succeed("localhost"))
894 * )
895 * const provider = ConfigProvider.fromUnknown({})
896 * Effect.runSync(host.parse(provider)) // => "localhost"
897 * ```
898 *
899 * @category constructors
900 * @since 2.0.0
901 */
902export function succeed<T>(value: T) {
903 return make(() => Effect.succeed(Result.succeed(value)))
904}
905
906/**
907 * Creates a config for a single string value.
908 *
909 * **When to use**
910 *
911 * Use when reading a single string env var or config key.
912 *
913 * **Details**
914 *
915 * Shortcut for `Config.schema(Schema.String, name)`.
916 *
917 * **Example** (Reading a string config)
918 *
919 * ```ts import.meta.vitest
920 * import { Config, ConfigProvider, Effect } from "effect"
921 *
922 * const host = Config.String("HOST")
923 *
924 * const provider = ConfigProvider.fromUnknown({ HOST: "localhost" })
925 * Effect.runSync(host.parse(provider)) // => "localhost"
926 * ```
927 *
928 * @see {@link NonEmptyString} – rejects empty strings
929 * @see {@link schema} – for more complex types
930 *
931 * @category constructors
932 * @since 2.0.0
933 */
934export function String(name?: string) {
935 return schema(Schema.String, name)
936}
937
938/**
939 * Creates a config for a non-empty string value. Fails if the value is an
940 * empty string.
941 *
942 * **When to use**
943 *
944 * Use to read a string config value that must contain at least one character.
945 *
946 * **Details**
947 *
948 * Shortcut for `Config.schema(Schema.NonEmptyString, name)`.
949 *
950 * @see {@link String} for allowing empty strings
951 *
952 * @category constructors
953 * @since 3.7.0
954 */
955export function NonEmptyString(name?: string) {
956 return schema(Schema.NonEmptyString, name)
957}
958
959/**
960 * Creates a config for a numeric value (including `NaN`, `Infinity`).
961 *
962 * **When to use**
963 *
964 * Use when you need config input to accept JavaScript's full number domain,
965 * including NaN and infinities, rather than reject non-finite values.
966 *
967 * **Details**
968 *
969 * Shortcut for `Config.schema(Schema.Number, name)`.
970 *
971 * @see {@link Finite} for rejecting `NaN` and `Infinity`
972 * @see {@link Int} for accepting only integers
973 *
974 * @category constructors
975 * @since 2.0.0
976 */
977export function Number(name?: string) {
978 return schema(Schema.Number, name)
979}
980
981/**
982 * Creates a config for a finite number (rejects `NaN` and `Infinity`).
983 *
984 * **When to use**
985 *
986 * Use to read a numeric config value that must be finite.
987 *
988 * **Details**
989 *
990 * Shortcut for `Config.schema(Schema.Finite, name)`.
991 *
992 * @see {@link Number} for accepting `NaN` and `Infinity`
993 * @see {@link Int} for accepting only integers
994 *
995 * @category constructors
996 * @since 4.0.0
997 */
998export function Finite(name?: string) {
999 return schema(Schema.Finite, name)
1000}
1001
1002/**
1003 * Creates a config for an integer value. Rejects floats.
1004 *
1005 * **When to use**
1006 *
1007 * Use to read a numeric config value that must be an integer.
1008 *
1009 * **Details**
1010 *
1011 * Shortcut for `Config.schema(Schema.Int, name)`.
1012 *
1013 * @see {@link Number} for accepting any number
1014 * @see {@link Port} for accepting only integers in `1` through `65535`
1015 *
1016 * @category constructors
1017 * @since 4.0.0
1018 */
1019export function Int(name?: string) {
1020 return schema(Schema.Int, name)
1021}
1022
1023/**
1024 * Creates a config that only accepts a specific literal value.
1025 *
1026 * **When to use**
1027 *
1028 * Use to restrict a config to a single, specific literal value.
1029 *
1030 * **Details**
1031 *
1032 * Shortcut for `Config.schema(Schema.Literal(literal), name)`.
1033 *
1034 * **Example** (Restricting to a literal)
1035 *
1036 * ```ts import.meta.vitest
1037 * import { Config, ConfigProvider, Effect } from "effect"
1038 *
1039 * const env = Config.Literal("production", "ENV")
1040 * const provider = ConfigProvider.fromUnknown({ ENV: "production" })
1041 * Effect.runSync(env.parse(provider)) // => "production"
1042 * ```
1043 *
1044 * @see {@link Literals} – accepts multiple literal values
1045 * @category constructors
1046 * @since 2.0.0
1047 */
1048export function Literal<L extends SchemaAST.LiteralValue>(literal: L, name?: string) {
1049 return schema(Schema.Literal(literal), name)
1050}
1051
1052/**
1053 * Creates a config that only accepts one of the specified literal values.
1054 *
1055 * **When to use**
1056 *
1057 * Use to restrict a config to a fixed set of allowed literal values.
1058 *
1059 * **Details**
1060 *
1061 * Shortcut for `Config.schema(Schema.Literals(literals), name)`.
1062 *
1063 * **Example** (Restricting to a set of literals)
1064 *
1065 * ```ts import.meta.vitest
1066 * import { Config, ConfigProvider, Effect } from "effect"
1067 *
1068 * const env = Config.Literals(["development", "production"], "ENV")
1069 * const provider = ConfigProvider.fromUnknown({ ENV: "development" })
1070 * Effect.runSync(env.parse(provider)) // => "development"
1071 * ```
1072 *
1073 * @see {@link Literal} for accepting one specific literal value
1074 *
1075 * @category constructors
1076 * @since 4.0.0
1077 */
1078export function Literals<const L extends ReadonlyArray<SchemaAST.LiteralValue>>(literals: L, name?: string) {
1079 return schema(Schema.Literals(literals), name)
1080}
1081
1082/**
1083 * Creates a config for array values that may also be read from a separated string.
1084 *
1085 * **When to use**
1086 *
1087 * Use when you need to read either structural array input or a flat value such as
1088 * `"otlp,console"` from an environment variable.
1089 *
1090 * **Details**
1091 *
1092 * Pass a string or `ConfigProvider.Path` as the second argument to set the lookup
1093 * path. When no path is needed, pass the options object directly. The `separator`
1094 * defaults to `","`.
1095 *
1096 * **Example** (Reading a comma-separated array)
1097 *
1098 * ```ts import.meta.vitest
1099 * import { Config, ConfigProvider, Effect, Schema } from "effect"
1100 *
1101 * const config = Config.Array(Schema.String, "EXPORTERS")
1102 * const provider = ConfigProvider.fromEnv({ env: { EXPORTERS: "otlp,console" } })
1103 *
1104 * Effect.runSync(config.parse(provider)) // => ["otlp", "console"]
1105 * ```
1106 *
1107 * @see {@link Record} for key-value input from structural records or separated strings.
1108 * @category constructors
1109 * @since 4.0.0
1110 */
1111export function Array<V extends Schema.ConstraintCodec<unknown, unknown>>(
1112 value: V,
1113 options?: ArrayOptions
1114): Config<ReadonlyArray<V["Type"]>>
1115export function Array<V extends Schema.ConstraintCodec<unknown, unknown>>(
1116 value: V,
1117 path: string | Path,
1118 options?: ArrayOptions
1119): Config<ReadonlyArray<V["Type"]>>
1120export function Array<V extends Schema.ConstraintCodec<unknown, unknown>>(
1121 value: V,
1122 pathOrOptions?: string | Path | ArrayOptions,
1123 options?: ArrayOptions
1124) {
1125 const hasPath = isPath(pathOrOptions)
1126 const resolvedOptions = hasPath ? options : pathOrOptions
1127 const array = Schema.Array(value)
1128 const separator = resolvedOptions?.separator ?? ","
1129 const arrayString = Schema.String.pipe(
1130 Schema.decodeTo(Schema.toCodecStringTree(array), {
1131 decode: SchemaGetter.split(resolvedOptions),
1132 encode: SchemaGetter.compose(
1133 SchemaGetter.passthrough<ReadonlyArray<string>, Schema.StringTree>({ strict: false }),
1134 SchemaGetter.transform((input) => input.join(separator))
1135 )
1136 })
1137 )
1138 return schema(Schema.Union([arrayString, array]), hasPath ? pathOrOptions : undefined)
1139}
1140
1141/**
1142 * Creates a config for record values that may also be read from a separated key-value string.
1143 *
1144 * **When to use**
1145 *
1146 * Use when you need to read either structural record input or a flat value such as
1147 * `"service.name=my-service,service.version=1.0.0"` from an environment variable.
1148 *
1149 * **Details**
1150 *
1151 * Pass a string or `ConfigProvider.Path` as the third argument to set the lookup
1152 * path. When no path is needed, pass the options object directly. The `separator`
1153 * defaults to `","` and `keyValueSeparator` defaults to `"="`.
1154 *
1155 * **Example** (Reading a comma-separated record)
1156 *
1157 * ```ts import.meta.vitest
1158 * import { Config, ConfigProvider, Effect, Schema } from "effect"
1159 *
1160 * const config = Config.Record(Schema.String, Schema.String, "OTEL_RESOURCE_ATTRIBUTES")
1161 * const provider = ConfigProvider.fromEnv({
1162 * env: {
1163 * OTEL_RESOURCE_ATTRIBUTES:
1164 * "service.name=my-service,service.version=1.0.0,custom.attribute=value"
1165 * }
1166 * })
1167 *
1168 * const result = Effect.runSync(config.parse(provider))
1169 * result["service.name"] // => "my-service"
1170 * result["service.version"] // => "1.0.0"
1171 * result["custom.attribute"] // => "value"
1172 * ```
1173 *
1174 * @see {@link Array} for array input from structural arrays or separated strings.
1175 * @category constructors
1176 * @since 4.0.0
1177 */
1178export function Record<
1179 K extends Schema.Record.Key & Schema.ConstraintCodec<unknown, unknown>,
1180 V extends Schema.ConstraintCodec<unknown, unknown>
1181>(
1182 key: K,
1183 value: V,
1184 options?: RecordOptions
1185): Config<Schema.Record.Type<K, V>>
1186export function Record<
1187 K extends Schema.Record.Key & Schema.ConstraintCodec<unknown, unknown>,
1188 V extends Schema.ConstraintCodec<unknown, unknown>
1189>(
1190 key: K,
1191 value: V,
1192 path: string | Path,
1193 options?: RecordOptions
1194): Config<Schema.Record.Type<K, V>>
1195export function Record<
1196 K extends Schema.Record.Key & Schema.ConstraintCodec<unknown, unknown>,
1197 V extends Schema.ConstraintCodec<unknown, unknown>
1198>(
1199 key: K,
1200 value: V,
1201 pathOrOptions?: string | Path | RecordOptions,
1202 options?: RecordOptions
1203) {
1204 const hasPath = isPath(pathOrOptions)
1205 const record = Schema.Record(key, value)
1206 const split = SchemaTransformation.splitKeyValue(hasPath ? options : pathOrOptions)
1207 const recordString = Schema.String.pipe(
1208 Schema.decodeTo(Schema.toCodecStringTree(record), {
1209 decode: split.decode,
1210 encode: SchemaGetter.compose(
1211 SchemaGetter.passthrough<Record<string, string>, Schema.StringTree>({ strict: false }),
1212 split.encode
1213 )
1214 })
1215 )
1216 return schema(Schema.Union([record, recordString]), hasPath ? pathOrOptions : undefined)
1217}
1218
1219/**
1220 * Creates a config for a boolean value parsed from common string
1221 * representations.
1222 *
1223 * **When to use**
1224 *
1225 * Use to read boolean flags from string-like config sources.
1226 *
1227 * **Details**
1228 *
1229 * Accepted values: `true`, `false`, `yes`, `no`, `on`, `off`, `1`, `0`,
1230 * `y`, `n`.
1231 *
1232 * **Example** (Reading a boolean flag)
1233 *
1234 * ```ts import.meta.vitest
1235 * import { Config, ConfigProvider, Effect } from "effect"
1236 *
1237 * const program = Config.Boolean("FEATURE_FLAG")
1238 *
1239 * const provider = ConfigProvider.fromEnv({
1240 * env: {
1241 * FEATURE_FLAG: "yes"
1242 * }
1243 * })
1244 *
1245 * Effect.runSync(
1246 * program.pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider))
1247 * ) // => true
1248 * ```
1249 *
1250 * @category constructors
1251 * @since 2.0.0
1252 */
1253export function Boolean(name?: string) {
1254 return schema(Schema.BooleanLiterals, name)
1255}
1256
1257/**
1258 * Creates a config for a `Duration` value parsed from a human-readable
1259 * string.
1260 *
1261 * **When to use**
1262 *
1263 * Use to read time duration settings such as timeouts, intervals, or TTLs.
1264 *
1265 * **Details**
1266 *
1267 * Shortcut for `Config.schema(Schema.DurationFromString, name)`.
1268 *
1269 * Accepts any string that `Duration.fromInput` can parse (e.g.
1270 * `"10 seconds"`, `"500 millis"`, `"Infinity"`, `"-Infinity"`).
1271 *
1272 * **Example** (Reading a duration)
1273 *
1274 * ```ts import.meta.vitest
1275 * import { Config, ConfigProvider, Duration, Effect } from "effect"
1276 *
1277 * const program = Config.Duration("DURATION").pipe(Effect.map(Duration.toMillis))
1278 *
1279 * const provider = ConfigProvider.fromEnv({
1280 * env: {
1281 * DURATION: "10 seconds"
1282 * }
1283 * })
1284 *
1285 * Effect.runSync(
1286 * program.pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider))
1287 * ) // => 10000
1288 * ```
1289 *
1290 * @see {@link schema} for decoding configuration values with a custom codec
1291 *
1292 * @category constructors
1293 * @since 2.5.0
1294 */
1295export function Duration(name?: string) {
1296 return schema(Schema.DurationFromString, name)
1297}
1298
1299/**
1300 * Creates a config for an exact, human-readable byte-size value.
1301 *
1302 * **Details**
1303 *
1304 * Decimal symbols such as `kB` use powers of 1,000, while binary symbols such
1305 * as `KiB` use powers of 1,024.
1306 *
1307 * @category constructors
1308 * @since 4.0.0
1309 */
1310export function ByteSize(name?: string) {
1311 return schema(Schema.ByteSize, name)
1312}
1313
1314/**
1315 * Creates a config for a port number (integer in 1–65535).
1316 *
1317 * **When to use**
1318 *
1319 * Use to read network port settings that must be valid port numbers.
1320 *
1321 * **Details**
1322 *
1323 * Accepts integers from `1` through `65535`.
1324 *
1325 * **Example** (Reading a port)
1326 *
1327 * ```ts import.meta.vitest
1328 * import { Config, ConfigProvider, Effect } from "effect"
1329 *
1330 * const program = Config.Port("PORT")
1331 *
1332 * const provider = ConfigProvider.fromEnv({
1333 * env: {
1334 * PORT: "8080"
1335 * }
1336 * })
1337 *
1338 * Effect.runSync(
1339 * program.pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider))
1340 * ) // => 8080
1341 * ```
1342 *
1343 * @see {@link Int} for integer config values outside the port range
1344 *
1345 * @category constructors
1346 * @since 3.16.0
1347 */
1348export function Port(name?: string) {
1349 return schema(PortSchema, name)
1350}
1351
1352/**
1353 * Creates a config for a log level string.
1354 *
1355 * **When to use**
1356 *
1357 * Use to read Effect log-level settings from configuration.
1358 *
1359 * **Details**
1360 *
1361 * Accepted values: `"All"`, `"Fatal"`, `"Error"`, `"Warn"`, `"Info"`,
1362 * `"Debug"`, `"Trace"`, `"None"`.
1363 *
1364 * **Example** (Reading a log level)
1365 *
1366 * ```ts import.meta.vitest
1367 * import { Config, ConfigProvider, Effect } from "effect"
1368 *
1369 * const program = Config.LogLevel("LOG_LEVEL")
1370 *
1371 * const provider = ConfigProvider.fromEnv({
1372 * env: {
1373 * LOG_LEVEL: "Info"
1374 * }
1375 * })
1376 *
1377 * Effect.runSync(
1378 * program.pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider))
1379 * ) // => "Info"
1380 * ```
1381 *
1382 * @category constructors
1383 * @since 2.0.0
1384 */
1385export function LogLevel(name?: string) {
1386 return schema(LogLevelSchema, name)
1387}
1388
1389/**
1390 * Creates a config for a redacted string value. The parsed result is wrapped
1391 * in a `Redacted` container that hides the value from logs and `toString`.
1392 *
1393 * **When to use**
1394 *
1395 * Use to read secret string settings that should not be exposed in logs or
1396 * string output.
1397 *
1398 * **Details**
1399 *
1400 * Shortcut for `Config.schema(Schema.Redacted(Schema.String), name)`.
1401 *
1402 * **Example** (Reading a secret)
1403 *
1404 * ```ts import.meta.vitest
1405 * import { Config, ConfigProvider, Effect } from "effect"
1406 *
1407 * const program = Config.Redacted("API_KEY").pipe(Effect.map(String))
1408 *
1409 * const provider = ConfigProvider.fromEnv({
1410 * env: {
1411 * API_KEY: "sk-1234567890abcdef"
1412 * }
1413 * })
1414 *
1415 * Effect.runSync(
1416 * program.pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider))
1417 * ) // => "<redacted>"
1418 * ```
1419 *
1420 * @see {@link String} for non-secret string settings
1421 *
1422 * @category constructors
1423 * @since 2.0.0
1424 */
1425export function Redacted(name?: string) {
1426 return schema(Schema.Redacted(Schema.String), name)
1427}
1428
1429/**
1430 * Creates a config for a `URL` value parsed from a string.
1431 *
1432 * **When to use**
1433 *
1434 * Use to read configuration values that must be valid URL strings.
1435 *
1436 * **Details**
1437 *
1438 * This is a shortcut for `Config.schema(Schema.URL, name)`.
1439 *
1440 * **Gotchas**
1441 *
1442 * Fails if the string cannot be parsed by the `URL` constructor.
1443 *
1444 * **Example** (Reading a URL)
1445 *
1446 * ```ts import.meta.vitest
1447 * import { Config, ConfigProvider, Effect } from "effect"
1448 *
1449 * const program = Config.URL("URL").pipe(Effect.map((url) => url.href))
1450 *
1451 * const provider = ConfigProvider.fromEnv({
1452 * env: {
1453 * URL: "https://example.com"
1454 * }
1455 * })
1456 *
1457 * Effect.runSync(
1458 * program.pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider))
1459 * ) // => "https://example.com/"
1460 * ```
1461 *
1462 * @see {@link schema} for decoding configuration values with a custom codec
1463 *
1464 * @category constructors
1465 * @since 3.11.0
1466 */
1467export function URL(name?: string) {
1468 return schema(Schema.URL, name)
1469}
1470
1471/**
1472 * Creates a config for a `Date` value parsed from a string.
1473 *
1474 * **When to use**
1475 *
1476 * Use to read date settings that must parse to valid `Date` values.
1477 *
1478 * **Details**
1479 *
1480 * Shortcut for `Config.schema(Schema.Date, name)`.
1481 *
1482 * **Gotchas**
1483 *
1484 * Fails with a `SchemaError` if the string produces an invalid `Date`.
1485 *
1486 * **Example** (Reading a date)
1487 *
1488 * ```ts import.meta.vitest
1489 * import { Config, ConfigProvider, Effect } from "effect"
1490 *
1491 * const createdAt = Config.Date("CREATED_AT")
1492 *
1493 * const provider = ConfigProvider.fromUnknown({ CREATED_AT: "2024-01-15" })
1494 * Effect.runSync(createdAt.parse(provider)).toISOString() // => "2024-01-15T00:00:00.000Z"
1495 * ```
1496 *
1497 * @category constructors
1498 * @since 2.0.0
1499 */
1500export function Date(name?: string) {
1501 return schema(Schema.Date, name)
1502}
1503
1504/**
1505 * Scopes a config under a named prefix.
1506 *
1507 * **When to use**
1508 *
1509 * Use when you need to group related config keys under a common namespace.
1510 *
1511 * **Details**
1512 *
1513 * The prefix is prepended to every key the inner config reads. With
1514 * `fromUnknown` this means an extra object level; with `fromEnv` it means
1515 * a `_`-separated prefix on env var names.
1516 *
1517 * Multiple `nested` calls compose: the outermost name becomes the
1518 * outermost path segment.
1519 *
1520 * **Example** (Nesting a struct config under `"database"`)
1521 *
1522 * ```ts import.meta.vitest
1523 * import { Config, ConfigProvider, Effect } from "effect"
1524 *
1525 * const dbConfig = Config.all({
1526 * host: Config.String("host"),
1527 * port: Config.Number("port")
1528 * }).pipe(Config.nested("database"))
1529 *
1530 * const provider = ConfigProvider.fromUnknown({
1531 * database: { host: "localhost", port: "5432" }
1532 * })
1533 * Effect.runSync(dbConfig.parse(provider)) // => { host: "localhost", port: 5432 }
1534 * ```
1535 *
1536 * **Example** (Reading env vars with a nested prefix)
1537 *
1538 * ```ts import.meta.vitest
1539 * import { Config, ConfigProvider, Effect } from "effect"
1540 *
1541 * const host = Config.String("host").pipe(Config.nested("database"))
1542 *
1543 * const provider = ConfigProvider.fromEnv({
1544 * env: { database_host: "localhost" }
1545 * })
1546 * Effect.runSync(host.parse(provider)) // => "localhost"
1547 * ```
1548 *
1549 * @see {@link all} – combine multiple configs into a struct
1550 * @see {@link schema} – read structured config from a schema
1551 *
1552 * @category combinators
1553 * @since 2.0.0
1554 */
1555export const nested: {
1556 (name: string): <A>(self: Config<A>) => Config<A>
1557 <A>(self: Config<A>, name: string): Config<A>
1558} = dual(
1559 2,
1560 <A>(self: Config<A>, name: string): Config<A> =>
1561 make((provider, pathPrefix) => evaluateAt(self, provider, [...pathPrefix, name]))
1562)