import type * as Brand from "../Brand.js" import * as Chunk from "../Chunk.js" import type * as Config from "../Config.js" import * as ConfigError from "../ConfigError.js" import * as Duration from "../Duration.js" import * as Either from "../Either.js" import type { LazyArg } from "../Function.js" import { constTrue, dual, pipe } from "../Function.js" import type * as HashMap from "../HashMap.js" import * as HashSet from "../HashSet.js" import { formatUnknown } from "../Inspectable.js" import type * as LogLevel from "../LogLevel.js" import * as Option from "../Option.js" import { hasProperty, type Predicate, type Refinement } from "../Predicate.js" import type * as Redacted from "../Redacted.js" import type * as Secret from "../Secret.js" import * as configError from "./configError.js" import * as core from "./core.js" import * as defaultServices from "./defaultServices.js" import * as effectable from "./effectable.js" import * as OpCodes from "./opCodes/config.js" import * as redacted_ from "./redacted.js" import * as InternalSecret from "./secret.js" const ConfigSymbolKey = "effect/Config" /** @internal */ export const ConfigTypeId: Config.ConfigTypeId = Symbol.for( ConfigSymbolKey ) as Config.ConfigTypeId /** @internal */ export type ConfigPrimitive = | Constant | Described | Fallback | Fail | Lazy | MapOrFail | Nested | Primitive | Sequence | Table | Zipped const configVariance = { /* c8 ignore next */ _A: (_: never) => _ } const proto = { ...effectable.CommitPrototype, [ConfigTypeId]: configVariance, commit(this: Config.Config) { return defaultServices.config(this) } } /** @internal */ export type Op = Config.Config & Body & { readonly _tag: Tag } /** @internal */ export interface Constant extends Op }> {} /** @internal */ export interface Described extends Op readonly description: string }> {} /** @internal */ export interface Fallback extends Op readonly second: Config.Config readonly condition: Predicate }> {} /** @internal */ export interface Fail extends Op }> {} /** @internal */ export interface Lazy extends Op }> {} /** @internal */ export interface MapOrFail extends Op mapOrFail(value: unknown): Either.Either }> {} /** @internal */ export interface Nested extends Op }> {} /** @internal */ export interface Primitive extends Op }> {} /** @internal */ export interface Sequence extends Op }> {} /** @internal */ export interface Table extends Op }> {} /** @internal */ export interface Zipped extends Op readonly right: Config.Config zip(a: unknown, b: unknown): unknown }> {} /** @internal */ export const boolean = (name?: string): Config.Config => { const config = primitive( "a boolean property", (text) => { switch (text) { case "true": case "yes": case "on": case "1": { return Either.right(true) } case "false": case "no": case "off": case "0": { return Either.right(false) } default: { const error = configError.InvalidData( [], `Expected a boolean value but received ${formatUnknown(text)}` ) return Either.left(error) } } } ) return name === undefined ? config : nested(config, name) } /** @internal */ export const url = (name?: string): Config.Config => { const config = primitive( "an URL property", (text) => Either.try({ try: () => new URL(text), catch: (_) => configError.InvalidData([], `Expected an URL value but received ${formatUnknown(text)}`) }) ) return name === undefined ? config : nested(config, name) } /** @internal */ export const port = (name?: string): Config.Config => { const config = primitive( "a network port property", (text) => { const result = Number(text) if ( Number.isNaN(result) || result.toString() !== text.toString() || !Number.isInteger(result) || result < 1 || result > 65535 ) { return Either.left( configError.InvalidData( [], `Expected a network port value but received ${formatUnknown(text)}` ) ) } return Either.right(result) } ) return name === undefined ? config : nested(config, name) } /** @internal */ export const array = (config: Config.Config, name?: string): Config.Config> => { return pipe(chunk(config, name), map(Chunk.toArray)) } /** @internal */ export const chunk = (config: Config.Config, name?: string): Config.Config> => { return map(name === undefined ? repeat(config) : nested(repeat(config), name), Chunk.unsafeFromArray) } /** @internal */ export const date = (name?: string): Config.Config => { const config = primitive( "a date property", (text) => { const result = Date.parse(text) if (Number.isNaN(result)) { return Either.left( configError.InvalidData( [], `Expected a Date value but received ${formatUnknown(text)}` ) ) } return Either.right(new Date(result)) } ) return name === undefined ? config : nested(config, name) } /** @internal */ export const fail = (message: string): Config.Config => { const fail = Object.create(proto) fail._tag = OpCodes.OP_FAIL fail.message = message fail.parse = () => Either.left(configError.Unsupported([], message)) return fail } /** @internal */ export const number = (name?: string): Config.Config => { const config = primitive( "a number property", (text) => { const result = Number(text) if (Number.isNaN(result)) { return Either.left( configError.InvalidData( [], `Expected a number value but received ${formatUnknown(text)}` ) ) } return Either.right(result) } ) return name === undefined ? config : nested(config, name) } /** @internal */ export const integer = (name?: string): Config.Config => { const config = primitive( "an integer property", (text) => { const result = Number(text) if (!Number.isInteger(result)) { return Either.left( configError.InvalidData( [], `Expected an integer value but received ${formatUnknown(text)}` ) ) } return Either.right(result) } ) return name === undefined ? config : nested(config, name) } /** @internal */ export const literal = >(...literals: Literals) => ( name?: string ): Config.Config => { const valuesString = literals.map(String).join(", ") const config = primitive(`one of (${valuesString})`, (text) => { const found = literals.find((value) => String(value) === text) if (found === undefined) { return Either.left( configError.InvalidData( [], `Expected one of (${valuesString}) but received ${formatUnknown(text)}` ) ) } return Either.right(found) }) return name === undefined ? config : nested(config, name) } /** @internal */ export const logLevel = (name?: string): Config.Config => { const config = mapOrFail(string(), (value) => { const label = value.toUpperCase() const level = core.allLogLevels.find((level) => level.label === label) return level === undefined ? Either.left( configError.InvalidData([], `Expected a log level but received ${formatUnknown(value)}`) ) : Either.right(level) }) return name === undefined ? config : nested(config, name) } /** @internal */ export const duration = (name?: string): Config.Config => { const config = mapOrFail(string(), (value) => { const duration = Duration.decodeUnknown(value) return Either.fromOption( duration, () => configError.InvalidData([], `Expected a duration but received ${formatUnknown(value)}`) ) }) return name === undefined ? config : nested(config, name) } /** @internal */ export const map = dual< (f: (a: A) => B) => (self: Config.Config) => Config.Config, (self: Config.Config, f: (a: A) => B) => Config.Config >(2, (self, f) => mapOrFail(self, (a) => Either.right(f(a)))) /** @internal */ export const mapAttempt = dual< (f: (a: A) => B) => (self: Config.Config) => Config.Config, (self: Config.Config, f: (a: A) => B) => Config.Config >(2, (self, f) => mapOrFail(self, (a) => { try { return Either.right(f(a)) } catch (error) { return Either.left( configError.InvalidData( [], error instanceof Error ? error.message : `${error}` ) ) } })) /** @internal */ export const mapOrFail = dual< (f: (a: A) => Either.Either) => (self: Config.Config) => Config.Config, (self: Config.Config, f: (a: A) => Either.Either) => Config.Config >(2, (self, f) => { const mapOrFail = Object.create(proto) mapOrFail._tag = OpCodes.OP_MAP_OR_FAIL mapOrFail.original = self mapOrFail.mapOrFail = f return mapOrFail }) /** @internal */ export const nested = dual< (name: string) => (self: Config.Config) => Config.Config, (self: Config.Config, name: string) => Config.Config >(2, (self, name) => { const nested = Object.create(proto) nested._tag = OpCodes.OP_NESTED nested.name = name nested.config = self return nested }) /** @internal */ export const orElse = dual< (that: LazyArg>) => (self: Config.Config) => Config.Config, (self: Config.Config, that: LazyArg>) => Config.Config >(2, (self, that) => { const fallback = Object.create(proto) fallback._tag = OpCodes.OP_FALLBACK fallback.first = self fallback.second = suspend(that) fallback.condition = constTrue return fallback }) /** @internal */ export const orElseIf = dual< ( options: { readonly if: Predicate readonly orElse: LazyArg> } ) => (self: Config.Config) => Config.Config, ( self: Config.Config, options: { readonly if: Predicate readonly orElse: LazyArg> } ) => Config.Config >(2, (self, options) => { const fallback = Object.create(proto) fallback._tag = OpCodes.OP_FALLBACK fallback.first = self fallback.second = suspend(options.orElse) fallback.condition = options.if return fallback }) /** @internal */ export const option = (self: Config.Config): Config.Config> => { return pipe( self, map(Option.some), orElseIf({ orElse: () => succeed(Option.none()), if: ConfigError.isMissingDataOnly }) ) } /** @internal */ export const primitive = ( description: string, parse: (text: string) => Either.Either ): Config.Config => { const primitive = Object.create(proto) primitive._tag = OpCodes.OP_PRIMITIVE primitive.description = description primitive.parse = parse return primitive } /** @internal */ export const repeat = (self: Config.Config): Config.Config> => { const repeat = Object.create(proto) repeat._tag = OpCodes.OP_SEQUENCE repeat.config = self return repeat } /** @internal */ export const secret = (name?: string): Config.Config => { const config = primitive( "a secret property", (text) => Either.right(InternalSecret.fromString(text)) ) return name === undefined ? config : nested(config, name) } /** @internal */ export const redacted = ( nameOrConfig?: string | Config.Config ): Config.Config> => { const config: Config.Config = isConfig(nameOrConfig) ? nameOrConfig : string(nameOrConfig) return map(config, redacted_.make) } /** @internal */ export const branded: { >( constructor: Brand.Brand.Constructor ): (config: Config.Config) => Config.Config >( name: string | undefined, constructor: Brand.Brand.Constructor ): Config.Config >( config: Config.Config, constructor: Brand.Brand.Constructor ): Config.Config } = dual(2, >( nameOrConfig: Config.Config> | string | undefined, constructor: B ) => { const config: Config.Config = isConfig(nameOrConfig) ? nameOrConfig : string(nameOrConfig) return mapOrFail(config, (a) => constructor.either(a).pipe( Either.mapLeft((brandErrors) => configError.InvalidData([], brandErrors.map((brandError) => brandError.message).join("\n")) ) )) }) /** @internal */ export const hashSet = (config: Config.Config, name?: string): Config.Config> => { const newConfig = map(chunk(config), HashSet.fromIterable) return name === undefined ? newConfig : nested(newConfig, name) } /** @internal */ export const string = (name?: string): Config.Config => { const config = primitive( "a text property", Either.right ) return name === undefined ? config : nested(config, name) } /** @internal */ export const nonEmptyString = (name?: string): Config.Config => { const config = primitive( "a non-empty text property", Either.liftPredicate((text) => text.length > 0, () => configError.MissingData([], "Expected a non-empty string")) ) return name === undefined ? config : nested(config, name) } /** @internal */ export const all = > | Record>>( arg: Arg ): Config.Config< [Arg] extends [ReadonlyArray>] ? { -readonly [K in keyof Arg]: [Arg[K]] extends [Config.Config] ? A : never } : [Arg] extends [Iterable>] ? Array : [Arg] extends [Record>] ? { -readonly [K in keyof Arg]: [Arg[K]] extends [Config.Config] ? A : never } : never > => { if (Array.isArray(arg)) { return tuple(arg) as any } else if (Symbol.iterator in arg) { return tuple([...(arg as Iterable>)]) as any } return struct(arg) as any } const struct = >>(r: NER): Config.Config< { [K in keyof NER]: [NER[K]] extends [{ [ConfigTypeId]: { _A: (_: never) => infer A } }] ? A : never } > => { const entries = Object.entries(r) let result = pipe(entries[0][1], map((value) => ({ [entries[0][0]]: value }))) if (entries.length === 1) { return result as any } const rest = entries.slice(1) for (const [key, config] of rest) { result = pipe( result, zipWith(config, (record, value) => ({ ...record, [key]: value })) ) } return result as any } /** @internal */ export const succeed = (value: A): Config.Config => { const constant = Object.create(proto) constant._tag = OpCodes.OP_CONSTANT constant.value = value constant.parse = () => Either.right(value) return constant } /** @internal */ export const suspend = (config: LazyArg>): Config.Config => { const lazy = Object.create(proto) lazy._tag = OpCodes.OP_LAZY lazy.config = config return lazy } /** @internal */ export const sync = (value: LazyArg): Config.Config => { return suspend(() => succeed(value())) } /** @internal */ export const hashMap = (config: Config.Config, name?: string): Config.Config> => { const table = Object.create(proto) table._tag = OpCodes.OP_HASHMAP table.valueConfig = config return name === undefined ? table : nested(table, name) } /** @internal */ export const isConfig = (u: unknown): u is Config.Config => hasProperty(u, ConfigTypeId) /** @internal */ const tuple = >>(tuple: T): Config.Config< { [K in keyof T]: [T[K]] extends [Config.Config] ? A : never } > => { if (tuple.length === 0) { return succeed([]) as any } if (tuple.length === 1) { return map(tuple[0], (x) => [x]) as any } let result = map(tuple[0], (x) => [x]) for (let i = 1; i < tuple.length; i++) { const config = tuple[i] result = pipe( result, zipWith(config, (tuple, value) => [...tuple, value]) ) as any } return result as any } /** * @internal */ export const unwrap = (wrapped: Config.Config.Wrap): Config.Config => { if (isConfig(wrapped)) { return wrapped } return struct( Object.fromEntries( Object.entries(wrapped).map(([k, a]) => [k, unwrap(a as any)]) ) ) as any } /** @internal */ export const validate = dual< { (options: { readonly message: string readonly validation: Refinement }): (self: Config.Config) => Config.Config (options: { readonly message: string readonly validation: Predicate }): (self: Config.Config) => Config.Config }, { (self: Config.Config, options: { readonly message: string readonly validation: Refinement }): Config.Config (self: Config.Config, options: { readonly message: string readonly validation: Predicate }): Config.Config } >(2, (self: Config.Config, { message, validation }: { readonly message: string readonly validation: Predicate }) => mapOrFail(self, (a) => { if (validation(a)) { return Either.right(a) } return Either.left(configError.InvalidData([], message)) })) /** @internal */ export const withDefault = dual< (def: A2) => (self: Config.Config) => Config.Config, (self: Config.Config, def: A2) => Config.Config >(2, (self, def) => orElseIf(self, { orElse: () => succeed(def), if: ConfigError.isMissingDataOnly })) /** @internal */ export const withDescription = dual< (description: string) => (self: Config.Config) => Config.Config, (self: Config.Config, description: string) => Config.Config >(2, (self, description) => { const described = Object.create(proto) described._tag = OpCodes.OP_DESCRIBED described.config = self described.description = description return described }) /** @internal */ export const zip = dual< (that: Config.Config) => (self: Config.Config) => Config.Config<[A, B]>, (self: Config.Config, that: Config.Config) => Config.Config<[A, B]> >(2, (self, that) => zipWith(self, that, (a, b) => [a, b])) /** @internal */ export const zipWith = dual< (that: Config.Config, f: (a: A, b: B) => C) => (self: Config.Config) => Config.Config, (self: Config.Config, that: Config.Config, f: (a: A, b: B) => C) => Config.Config >(3, (self, that, f) => { const zipWith = Object.create(proto) zipWith._tag = OpCodes.OP_ZIP_WITH zipWith.left = self zipWith.right = that zipWith.zip = f return zipWith })