import { describe, it } from "@effect/vitest" import { deepStrictEqual } from "@effect/vitest/utils" import { ConfigProvider, Effect, FileSystem, Layer, Path, PlatformError, Result } from "effect" async function assertSuccess( provider: ConfigProvider.ConfigProvider, path: ConfigProvider.Path, expected: ConfigProvider.Node | undefined ) { const r = Effect.result(provider.load(path)) deepStrictEqual(await Effect.runPromise(r), Result.succeed(expected)) } // async function assertFailure( // provider: ConfigProvider.ConfigProvider, // path: ConfigProvider.Path, // expected: ConfigProvider.SourceError // ) { // const r = Effect.result(provider.load(path)) // deepStrictEqual(await Effect.runPromise(r), Result.fail(expected)) // } describe("ConfigProvider", () => { it("orElse", async () => { const provider1 = ConfigProvider.fromEnv({ env: { "A": "value1" } }) const provider2 = ConfigProvider.fromEnv({ env: { "B": "value2" } }) const provider = provider1.pipe(ConfigProvider.orElse(provider2)) await assertSuccess(provider, ["A"], ConfigProvider.makeValue("value1")) await assertSuccess(provider, ["B"], ConfigProvider.makeValue("value2")) }) it("orElse does not fall back on SourceError", async () => { const error = new ConfigProvider.SourceError({ message: "io down" }) const primary = ConfigProvider.make(() => Effect.fail(error)) const fallback = ConfigProvider.fromEnv({ env: { KEY: "fallback" } }) const provider = primary.pipe(ConfigProvider.orElse(fallback)) const r = await Effect.runPromise(Effect.result(provider.load(["KEY"]))) deepStrictEqual(r, Result.fail(error)) }) it("orElse applies each operand's transformations", async () => { const primary = ConfigProvider.fromEnv({ env: { "DATABASE_HOST": "from-env" } }).pipe(ConfigProvider.constantCase) const fallback = ConfigProvider.fromEnv({ env: { "APP_PORT": "3000" } }).pipe(ConfigProvider.nested("APP")) const provider = primary.pipe(ConfigProvider.orElse(fallback)) await assertSuccess(provider, ["databaseHost"], ConfigProvider.makeValue("from-env")) await assertSuccess(provider, ["PORT"], ConfigProvider.makeValue("3000")) }) it("mapInput distributes over orElse", async () => { const appendSuffix = ConfigProvider.mapInput((path) => path.map((seg) => typeof seg === "string" ? `${seg}_SUFFIX` : seg) ) const primary = ConfigProvider.fromEnv({ env: { "prefix_SUFFIX_KEY_SUFFIX": "primary" } }).pipe(ConfigProvider.nested("prefix")) const fallback = ConfigProvider.fromEnv({ env: { "fallback_SUFFIX_KEY_SUFFIX": "fallback", "fallback_SUFFIX_OTHER_SUFFIX": "fallback" } }).pipe(ConfigProvider.nested("fallback")) const provider = primary.pipe(ConfigProvider.orElse(fallback), appendSuffix) await assertSuccess(provider, ["KEY"], ConfigProvider.makeValue("primary")) await assertSuccess(provider, ["OTHER"], ConfigProvider.makeValue("fallback")) }) it("nested distributes over orElse", async () => { const primary = ConfigProvider.fromEnv({ env: { "app_DATABASE_HOST": "primary" } }).pipe(ConfigProvider.constantCase) const fallback = ConfigProvider.fromEnv({ env: { "app_PORT": "fallback" } }) const provider = primary.pipe(ConfigProvider.orElse(fallback), ConfigProvider.nested("app")) await assertSuccess(provider, ["databaseHost"], ConfigProvider.makeValue("primary")) await assertSuccess(provider, ["PORT"], ConfigProvider.makeValue("fallback")) }) it("orElse falls back when the primary env value is empty", async () => { const primary = ConfigProvider.fromEnv({ env: { "KEY": "" } }) const fallback = ConfigProvider.fromUnknown({ KEY: "fallback" }) const provider = primary.pipe(ConfigProvider.orElse(fallback)) await assertSuccess(provider, ["KEY"], ConfigProvider.makeValue("fallback")) }) it("constantCase", async () => { const provider = ConfigProvider.constantCase(ConfigProvider.fromEnv({ env: { "CONSTANT_CASE": "value1" } })) await assertSuccess(provider, ["constant.case"], ConfigProvider.makeValue("value1")) }) it("constantCase uses config casing for numeric word groups", async () => { const provider = ConfigProvider.constantCase(ConfigProvider.fromEnv({ env: { "API_V2_XML": "value1", "FIELD2_VALUE": "value2" } })) await assertSuccess(provider, ["api-v2 xml"], ConfigProvider.makeValue("value1")) await assertSuccess(provider, ["field2Value"], ConfigProvider.makeValue("value2")) }) describe("mapInput", () => { it("two mappings", async () => { const appendA = ConfigProvider.mapInput((path) => path.map((sn) => typeof sn === "string" ? sn + "_A" : sn)) const appendB = ConfigProvider.mapInput((path) => path.map((sn) => typeof sn === "string" ? sn + "_B" : sn)) const provider = ConfigProvider.fromEnv({ env: { "KEY_A_B": "value" } }).pipe(appendA, appendB) await assertSuccess(provider, ["KEY"], ConfigProvider.makeValue("value")) }) }) describe("nested", () => { it("should add a prefix to the path", async () => { const provider = ConfigProvider.fromEnv({ env: { "prefix_A": "value" } }).pipe(ConfigProvider.nested("prefix")) await assertSuccess(provider, ["A"], ConfigProvider.makeValue("value")) }) it("constantCase + nested", async () => { const provider = ConfigProvider.fromEnv({ env: { "prefix_KEY_WITH_DOTS": "value" } }).pipe(ConfigProvider.constantCase, ConfigProvider.nested("prefix")) await assertSuccess(provider, ["key.with.dots"], ConfigProvider.makeValue("value")) }) it("nested + constantCase", async () => { const provider = ConfigProvider.fromEnv({ env: { "PREFIX_WITH_DOTS_KEY_WITH_DOTS": "value" } }).pipe(ConfigProvider.nested("prefix.with.dots"), ConfigProvider.constantCase) await assertSuccess(provider, ["key.with.dots"], ConfigProvider.makeValue("value")) }) it("multiple nested calls compose as wrappers", async () => { const provider = ConfigProvider.fromEnv({ env: { "b_a_KEY": "value" } }).pipe(ConfigProvider.nested("a"), ConfigProvider.nested("b")) await assertSuccess(provider, ["KEY"], ConfigProvider.makeValue("value")) }) it("mapInput after nested transforms the full path", async () => { const appendLeaf = ConfigProvider.mapInput((path) => [...path, "leaf"]) const provider = ConfigProvider.fromEnv({ env: { "app_KEY_leaf": "value" } }).pipe(ConfigProvider.nested("app"), appendLeaf) await assertSuccess(provider, ["KEY"], ConfigProvider.makeValue("value")) }) }) describe("fromEnv", () => { it("env without an underscore", async () => { const env = { A: "value1" } const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, ["A"], ConfigProvider.makeValue("value1")) }) it("missing key returns undefined", async () => { const env = { A: "value1" } const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, ["missing"], undefined) }) it("treats empty string values as missing while preserving structure by default", async () => { const env = { A: "", B: "value1" } const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, [], ConfigProvider.makeRecord(new Set(["A", "B"]))) await assertSuccess(provider, ["A"], undefined) await assertSuccess(provider, ["B"], ConfigProvider.makeValue("value1")) }) it("preserves empty strings when requested", async () => { const env = { A: "" } const provider = ConfigProvider.fromEnv({ env, preserveEmptyStrings: true }) await assertSuccess(provider, [], ConfigProvider.makeRecord(new Set(["A"]))) await assertSuccess(provider, ["A"], ConfigProvider.makeValue("")) }) it("treats empty co-located record values as missing and preserves children", async () => { const env = { A: "", A_B: "value1" } const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, ["A"], ConfigProvider.makeRecord(new Set(["B"]))) await assertSuccess(provider, ["A", "B"], ConfigProvider.makeValue("value1")) }) it("preserves empty co-located record values when requested", async () => { const env = { A: "", A_B: "value1" } const provider = ConfigProvider.fromEnv({ env, preserveEmptyStrings: true }) await assertSuccess(provider, ["A"], ConfigProvider.makeRecord(new Set(["B"]), "")) await assertSuccess(provider, ["A", "B"], ConfigProvider.makeValue("value1")) }) it("treats empty co-located array values as missing and preserves children", async () => { const env = { A: "", A_0: "value1" } const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, ["A"], ConfigProvider.makeArray(1)) await assertSuccess(provider, ["A", 0], ConfigProvider.makeValue("value1")) }) it("preserves empty array children structurally while treating their values as missing", async () => { const env = { A_0: "", A_1: "value1" } const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, ["A"], ConfigProvider.makeArray(2)) await assertSuccess(provider, ["A", 0], undefined) await assertSuccess(provider, ["A", 1], ConfigProvider.makeValue("value1")) }) it("direct lookup of a key containing underscores", async () => { const env = { NODE_ENV: "value1" } const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, ["NODE_ENV"], ConfigProvider.makeValue("value1")) }) it("single underscore creates nesting", async () => { const env = { NODE_ENV: "value1" } const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, ["NODE"], ConfigProvider.makeRecord(new Set(["ENV"]))) await assertSuccess(provider, ["NODE", "ENV"], ConfigProvider.makeValue("value1")) }) it("prefix can be both a value and a container", async () => { const env = { NODE: "value1", NODE_ENV: "value2" } const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, ["NODE"], ConfigProvider.makeRecord(new Set(["ENV"]), "value1")) await assertSuccess(provider, ["NODE", "ENV"], ConfigProvider.makeValue("value2")) await assertSuccess(provider, ["NODE_ENV"], ConfigProvider.makeValue("value2")) }) it("multiple nested children under the same prefix", async () => { const env = { NODE: "value1", NODE_ENV: "value2", NODE_PATH: "value3" } const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, ["NODE"], ConfigProvider.makeRecord(new Set(["ENV", "PATH"]), "value1")) await assertSuccess(provider, ["NODE", "ENV"], ConfigProvider.makeValue("value2")) await assertSuccess(provider, ["NODE", "PATH"], ConfigProvider.makeValue("value3")) await assertSuccess(provider, ["NODE_PATH"], ConfigProvider.makeValue("value3")) }) it("nested depth > 2 works", async () => { const env = { A_B_C: "value1" } const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, ["A"], ConfigProvider.makeRecord(new Set(["B"]))) await assertSuccess(provider, ["A", "B"], ConfigProvider.makeRecord(new Set(["C"]))) await assertSuccess(provider, ["A", "B", "C"], ConfigProvider.makeValue("value1")) }) it("array: dense numeric children yield an Array node", async () => { const env = { A_0: "value1", A_1: "value2" } const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, ["A"], ConfigProvider.makeArray(2)) await assertSuccess(provider, ["A", 0], ConfigProvider.makeValue("value1")) await assertSuccess(provider, ["A", 1], ConfigProvider.makeValue("value2")) }) it("array: non-dense numeric children still yield an Array node", async () => { const env = { A: "root", A_0: "value1", A_2: "value3" } const provider = ConfigProvider.fromEnv({ env }) // max index is 2 => length 3 (sparse is allowed) await assertSuccess(provider, ["A"], ConfigProvider.makeArray(3, "root")) await assertSuccess(provider, ["A", 0], ConfigProvider.makeValue("value1")) await assertSuccess(provider, ["A", 1], undefined) await assertSuccess(provider, ["A", 2], ConfigProvider.makeValue("value3")) }) it("array: parent can have a co-located value", async () => { const env = { A: "root", A_0: "value1", A_1: "value2" } const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, ["A"], ConfigProvider.makeArray(2, "root")) await assertSuccess(provider, ["A", 0], ConfigProvider.makeValue("value1")) await assertSuccess(provider, ["A", 1], ConfigProvider.makeValue("value2")) }) it("mixed children (numeric + non-numeric) yields Record", async () => { const env = { A_0: "value1", A_B: "value2" } const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, ["A"], ConfigProvider.makeRecord(new Set(["0", "B"]))) await assertSuccess(provider, ["A", 0], ConfigProvider.makeValue("value1")) await assertSuccess(provider, ["A", "B"], ConfigProvider.makeValue("value2")) }) it("double underscore produces an empty segment (no special handling)", async () => { const env = { A__B: "value1" } const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, ["A"], ConfigProvider.makeRecord(new Set([""]))) await assertSuccess(provider, ["A", ""], ConfigProvider.makeRecord(new Set(["B"]))) await assertSuccess(provider, ["A", "", "B"], ConfigProvider.makeValue("value1")) await assertSuccess(provider, ["A__B"], ConfigProvider.makeValue("value1")) }) it("empty segment node can be both a value and a container", async () => { const env = { A_: "value1", A__B: "value2" } const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, ["A", ""], ConfigProvider.makeRecord(new Set(["B"]), "value1")) await assertSuccess(provider, ["A", "", "B"], ConfigProvider.makeValue("value2")) }) it("leading underscore creates an empty first segment", async () => { const env = { _A: "value1" } const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, [], ConfigProvider.makeRecord(new Set([""]))) await assertSuccess(provider, [""], ConfigProvider.makeRecord(new Set(["A"]))) await assertSuccess(provider, ["", "A"], ConfigProvider.makeValue("value1")) await assertSuccess(provider, ["_A"], ConfigProvider.makeValue("value1")) }) it("trailing underscore creates an empty last segment", async () => { const env = { A_: "value1" } const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, ["A"], ConfigProvider.makeRecord(new Set([""]))) await assertSuccess(provider, ["A", ""], ConfigProvider.makeValue("value1")) await assertSuccess(provider, ["A_"], ConfigProvider.makeValue("value1")) }) it("cannot descend past a leaf (A exists, but A_B does not)", async () => { const env = { A: "value1" } const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, ["A"], ConfigProvider.makeValue("value1")) await assertSuccess(provider, ["A", "B"], undefined) await assertSuccess(provider, ["A_B"], undefined) }) it("direct lookup and nested lookup both work for the same env var", async () => { const env = { A_B: "value1" } const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, ["A_B"], ConfigProvider.makeValue("value1")) await assertSuccess(provider, ["A"], ConfigProvider.makeRecord(new Set(["B"]))) await assertSuccess(provider, ["A", "B"], ConfigProvider.makeValue("value1")) }) it("prefix value is preserved when there are multiple nested children", async () => { const env = { A: "root", A_B: "value1", A_C: "value2" } const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, ["A"], ConfigProvider.makeRecord(new Set(["B", "C"]), "root")) await assertSuccess(provider, ["A", "B"], ConfigProvider.makeValue("value1")) await assertSuccess(provider, ["A", "C"], ConfigProvider.makeValue("value2")) }) it("intermediate node can be both a value and a container (A_B and A_B_C)", async () => { const env = { A_B: "value1", A_B_C: "value2" } const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, ["A_B"], ConfigProvider.makeValue("value1")) await assertSuccess(provider, ["A"], ConfigProvider.makeRecord(new Set(["B"]))) await assertSuccess(provider, ["A", "B"], ConfigProvider.makeRecord(new Set(["C"]), "value1")) await assertSuccess(provider, ["A", "B", "C"], ConfigProvider.makeValue("value2")) }) it("numeric index: string and number segments should resolve the same", async () => { const env = { A_0: "value1" } const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, ["A", 0], ConfigProvider.makeValue("value1")) await assertSuccess(provider, ["A", "0"], ConfigProvider.makeValue("value1")) }) it("array: leading-zero indices are not numeric, so it yields a Record", async () => { const env = { A_01: "value1", A_1: "value2" } const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, ["A"], ConfigProvider.makeRecord(new Set(["01", "1"]))) await assertSuccess(provider, ["A", "01"], ConfigProvider.makeValue("value1")) await assertSuccess(provider, ["A", 1], ConfigProvider.makeValue("value2")) }) it("root path exposes top-level keys", async () => { const env = { A: "value1", B_C: "value2" } const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, [], ConfigProvider.makeRecord(new Set(["A", "B"]))) await assertSuccess(provider, ["A"], ConfigProvider.makeValue("value1")) await assertSuccess(provider, ["B"], ConfigProvider.makeRecord(new Set(["C"]))) }) it("underscore-only key creates empty segments", async () => { const env = { "_": "value1" } const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, ["_"], ConfigProvider.makeValue("value1")) await assertSuccess(provider, [], ConfigProvider.makeRecord(new Set([""]))) await assertSuccess(provider, [""], ConfigProvider.makeRecord(new Set([""]))) await assertSuccess(provider, ["", ""], ConfigProvider.makeValue("value1")) }) it("array: non-integer indices do not yield an Array node", async () => { const env = { "A_-1": "value1", "A_1.5": "value2" } const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, ["A"], ConfigProvider.makeRecord(new Set(["-1", "1.5"]))) }) }) describe("fromUnknown", () => { const provider = ConfigProvider.fromUnknown({ string: "value1", record: { key1: "value2", key2: { key3: "value3" } }, array: ["value4", { key4: "value5" }, ["value6"]], null: null, number: 42, boolean: true, bigint: 0n, undefined, unknown: Symbol("unknown") }) it("Root node", async () => { await assertSuccess( provider, [], ConfigProvider.makeRecord( new Set(["string", "record", "array", "null", "number", "boolean", "bigint", "undefined", "unknown"]) ) ) }) it("Exact leaf resolution", async () => { await assertSuccess(provider, ["string"], ConfigProvider.makeValue("value1")) await assertSuccess(provider, ["record", "key1"], ConfigProvider.makeValue("value2")) await assertSuccess(provider, ["array", 0], ConfigProvider.makeValue("value4")) await assertSuccess(provider, ["array", 1, "key4"], ConfigProvider.makeValue("value5")) await assertSuccess(provider, ["array", 2, 0], ConfigProvider.makeValue("value6")) }) it("treats empty string leaves as missing by default", async () => { const provider = ConfigProvider.fromUnknown({ a: "", b: "value1", record: { key: "" }, array: [""] }) await assertSuccess(provider, [], ConfigProvider.makeRecord(new Set(["a", "b", "record", "array"]))) await assertSuccess(provider, ["a"], undefined) await assertSuccess(provider, ["b"], ConfigProvider.makeValue("value1")) await assertSuccess(provider, ["record"], ConfigProvider.makeRecord(new Set(["key"]))) await assertSuccess(provider, ["record", "key"], undefined) await assertSuccess(provider, ["array"], ConfigProvider.makeArray(1)) await assertSuccess(provider, ["array", 0], undefined) }) it("preserves empty string leaves when requested", async () => { const provider = ConfigProvider.fromUnknown( { a: "", array: [""] }, { preserveEmptyStrings: true } ) await assertSuccess(provider, ["a"], ConfigProvider.makeValue("")) await assertSuccess(provider, ["array", 0], ConfigProvider.makeValue("")) }) it("Object detection", async () => { await assertSuccess(provider, ["record"], ConfigProvider.makeRecord(new Set(["key1", "key2"]))) await assertSuccess(provider, ["record", "key2"], ConfigProvider.makeRecord(new Set(["key3"]))) }) it("Array detection", async () => { await assertSuccess(provider, ["array"], ConfigProvider.makeArray(3)) await assertSuccess(provider, ["array", 2], ConfigProvider.makeArray(1)) }) it("should return undefined on non-existing paths", async () => { await assertSuccess(provider, ["string", "non-existing"], undefined) await assertSuccess(provider, ["record", "non-existing"], undefined) await assertSuccess(provider, ["array", 3, "non-existing"], undefined) }) it("null values", async () => { await assertSuccess(provider, ["null"], undefined) }) it("number values", async () => { await assertSuccess(provider, ["number"], ConfigProvider.makeValue("42")) }) it("boolean values", async () => { await assertSuccess(provider, ["boolean"], ConfigProvider.makeValue("true")) }) it("bigint values", async () => { await assertSuccess(provider, ["bigint"], ConfigProvider.makeValue("0")) }) it("undefined values", async () => { await assertSuccess(provider, ["undefined"], undefined) }) it("unknown values", async () => { await assertSuccess(provider, ["unknown"], ConfigProvider.makeValue("Symbol(unknown)")) }) }) describe("fromDotEnvContents", () => { it("comments are ignored", async () => { const provider = ConfigProvider.fromDotEnvContents(` # comments are ignored API_URL=https://api.example.com `) await assertSuccess(provider, [], ConfigProvider.makeRecord(new Set(["API"]))) await assertSuccess(provider, ["API_URL"], ConfigProvider.makeValue("https://api.example.com")) }) it("export is allowed", async () => { const provider = ConfigProvider.fromDotEnvContents(` export NODE_ENV=production `) await assertSuccess(provider, [], ConfigProvider.makeRecord(new Set(["NODE"]))) await assertSuccess(provider, ["NODE_ENV"], ConfigProvider.makeValue("production")) }) it("quoting is allowed", async () => { const provider = ConfigProvider.fromDotEnvContents(` NODE_ENV="production" `) await assertSuccess(provider, [], ConfigProvider.makeRecord(new Set(["NODE"]))) await assertSuccess(provider, ["NODE_ENV"], ConfigProvider.makeValue("production")) }) it("objects are supported", async () => { const provider = ConfigProvider.fromDotEnvContents(` OBJECT_key1=value1 OBJECT_key2=value2 `) await assertSuccess(provider, [], ConfigProvider.makeRecord(new Set(["OBJECT"]))) await assertSuccess(provider, ["OBJECT"], ConfigProvider.makeRecord(new Set(["key1", "key2"]))) await assertSuccess(provider, ["OBJECT", "key1"], ConfigProvider.makeValue("value1")) await assertSuccess(provider, ["OBJECT", "key2"], ConfigProvider.makeValue("value2")) }) it("treats empty string values as missing while preserving structure by default", async () => { const provider = ConfigProvider.fromDotEnvContents(` A= B=value1 `) await assertSuccess(provider, [], ConfigProvider.makeRecord(new Set(["A", "B"]))) await assertSuccess(provider, ["A"], undefined) await assertSuccess(provider, ["B"], ConfigProvider.makeValue("value1")) }) it("preserves empty strings when requested", async () => { const provider = ConfigProvider.fromDotEnvContents( ` A= `, { preserveEmptyStrings: true } ) await assertSuccess(provider, [], ConfigProvider.makeRecord(new Set(["A"]))) await assertSuccess(provider, ["A"], ConfigProvider.makeValue("")) }) it("a node may be both leaf and object", async () => { const provider = ConfigProvider.fromDotEnvContents(` OBJECT=value1 OBJECT_key1=value2 OBJECT_key2=value3 `) await assertSuccess(provider, [], ConfigProvider.makeRecord(new Set(["OBJECT"]))) await assertSuccess(provider, ["OBJECT"], ConfigProvider.makeRecord(new Set(["key1", "key2"]), "value1")) await assertSuccess(provider, ["OBJECT", "key1"], ConfigProvider.makeValue("value2")) await assertSuccess(provider, ["OBJECT", "key2"], ConfigProvider.makeValue("value3")) }) it("a node may be both leaf and array", async () => { const provider = ConfigProvider.fromDotEnvContents(` ARRAY=value1 ARRAY_0=value2 ARRAY_1=value3 `) await assertSuccess(provider, [], ConfigProvider.makeRecord(new Set(["ARRAY"]))) await assertSuccess(provider, ["ARRAY"], ConfigProvider.makeArray(2, "value1")) await assertSuccess(provider, ["ARRAY", 0], ConfigProvider.makeValue("value2")) await assertSuccess(provider, ["ARRAY", 1], ConfigProvider.makeValue("value3")) }) it("arrays are supported", async () => { const provider = ConfigProvider.fromDotEnvContents(` ARRAY_0=value1 ARRAY_1=value2 `) await assertSuccess(provider, [], ConfigProvider.makeRecord(new Set(["ARRAY"]))) await assertSuccess(provider, ["ARRAY"], ConfigProvider.makeArray(2)) await assertSuccess(provider, ["ARRAY", 0], ConfigProvider.makeValue("value1")) await assertSuccess(provider, ["ARRAY", 1], ConfigProvider.makeValue("value2")) }) it("expansion of environment variables is off by default", async () => { const provider = ConfigProvider.fromDotEnvContents(` PASSWORD="value" DB_PASS=$PASSWORD `) await assertSuccess(provider, [], ConfigProvider.makeRecord(new Set(["PASSWORD", "DB"]))) await assertSuccess(provider, ["PASSWORD"], ConfigProvider.makeValue("value")) await assertSuccess(provider, ["DB_PASS"], ConfigProvider.makeValue("$PASSWORD")) }) it("expansion of environment variables is supported", async () => { const provider = ConfigProvider.fromDotEnvContents( ` PASSWORD="value" DB_PASS=$PASSWORD `, { expandVariables: true } ) await assertSuccess(provider, [], ConfigProvider.makeRecord(new Set(["PASSWORD", "DB"]))) await assertSuccess(provider, ["PASSWORD"], ConfigProvider.makeValue("value")) await assertSuccess(provider, ["DB_PASS"], ConfigProvider.makeValue("value")) }) }) describe("fromDotEnv", () => { it("should load configuration from .env file", async () => { const provider = await Effect.runPromise( ConfigProvider.fromDotEnv().pipe( Effect.provide(FileSystem.layerNoop({ readFileString: (path) => Effect.succeed(`PATH=${path} A=1`) })) ) ) await assertSuccess(provider, ["PATH"], ConfigProvider.makeValue(".env")) await assertSuccess(provider, ["A"], ConfigProvider.makeValue("1")) }) it("should support `path` option to specify the path to the .env file", async () => { const provider = await Effect.runPromise( ConfigProvider.fromDotEnv({ path: "custom.env" }).pipe( Effect.provide(FileSystem.layerNoop({ readFileString: (path) => Effect.succeed(`CUSTOM_PATH=${path} A=1`) })) ) ) await assertSuccess(provider, ["CUSTOM_PATH"], ConfigProvider.makeValue("custom.env")) await assertSuccess(provider, ["A"], ConfigProvider.makeValue("1")) }) it("should support `expandVariables` option", async () => { const provider = await Effect.runPromise( ConfigProvider.fromDotEnv({ expandVariables: true }).pipe( Effect.provide(FileSystem.layerNoop({ readFileString: () => Effect.succeed(`PASSWORD=value DB_PASS=$PASSWORD`) })) ) ) await assertSuccess(provider, ["DB_PASS"], ConfigProvider.makeValue("value")) }) it("should treat empty string values as missing while preserving structure by default", async () => { const provider = await Effect.runPromise( ConfigProvider.fromDotEnv().pipe( Effect.provide(FileSystem.layerNoop({ readFileString: () => Effect.succeed("A=") })) ) ) await assertSuccess(provider, [], ConfigProvider.makeRecord(new Set(["A"]))) await assertSuccess(provider, ["A"], undefined) }) it("should support `preserveEmptyStrings` option", async () => { const provider = await Effect.runPromise( ConfigProvider.fromDotEnv({ preserveEmptyStrings: true }).pipe( Effect.provide(FileSystem.layerNoop({ readFileString: () => Effect.succeed("A=") })) ) ) await assertSuccess(provider, ["A"], ConfigProvider.makeValue("")) }) }) describe("fromDir", () => { const provider = ConfigProvider.fromDir({ rootPath: "/" }) const files: Record = { "/secret": "keepitsafe\n", "/SHOUTING": "value", "/integer": "123", "/nested/config": "hello" } const Fs = FileSystem.layerNoop({ readFileString(path) { if (path in files) { return Effect.succeed(files[path]) } return Effect.fail( PlatformError.systemError({ module: "FileSystem", _tag: "NotFound", method: "readFileString" }) ) }, readDirectory(_path) { // For the test, we only have files, no directories return Effect.fail( PlatformError.systemError({ module: "FileSystem", _tag: "NotFound", method: "readDirectory" }) ) } }) const Platform = Layer.mergeAll(Fs, Path.layer) const SetLayer = ConfigProvider.layer(provider).pipe( Layer.provide(Platform), Layer.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: { secret: "fail" } }))) ) const AddLayer = ConfigProvider.layerAdd(provider).pipe( Layer.provide(Platform), Layer.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: { secret: "shh", fallback: "value" } }))) ) it("reads config", async () => { const result = await Effect.runPromise( Effect.gen(function*() { const provider = yield* ConfigProvider.ConfigProvider const secret = yield* provider.load(["secret"]) const shouting = yield* provider.load(["SHOUTING"]) const integer = yield* provider.load(["integer"]) const nestedConfig = yield* provider.load(["nested", "config"]) return { secret, shouting, integer, nestedConfig } }).pipe(Effect.provide(SetLayer)) ) deepStrictEqual(result.secret, ConfigProvider.makeValue("keepitsafe")) deepStrictEqual(result.shouting, ConfigProvider.makeValue("value")) deepStrictEqual(result.integer, ConfigProvider.makeValue("123")) deepStrictEqual(result.nestedConfig, ConfigProvider.makeValue("hello")) const fallback = await Effect.runPromise( Effect.gen(function*() { const provider = yield* ConfigProvider.ConfigProvider return yield* provider.load(["fallback"]) }).pipe(Effect.provide(SetLayer)) ) deepStrictEqual(fallback, undefined) }) it("orElse falls back when fromDir path is missing", async () => { const provider = await Effect.runPromise( ConfigProvider.fromDir({ rootPath: "/" }).pipe( Effect.map((dir) => dir.pipe(ConfigProvider.orElse(ConfigProvider.fromEnv({ env: { fallback: "value" } })))), Effect.provide(Platform) ) ) await assertSuccess(provider, ["fallback"], ConfigProvider.makeValue("value")) }) it("treats empty files as missing by default", async () => { const Fs = FileSystem.layerNoop({ readFileString(path) { return path === "/empty" ? Effect.succeed("") : Effect.fail( PlatformError.systemError({ module: "FileSystem", _tag: "NotFound", method: "readFileString" }) ) }, readDirectory(path) { return path === "/" ? Effect.succeed(["empty"]) : Effect.fail( PlatformError.systemError({ module: "FileSystem", _tag: "NotFound", method: "readDirectory" }) ) } }) const provider = await Effect.runPromise( ConfigProvider.fromDir({ rootPath: "/" }).pipe(Effect.provide(Layer.mergeAll(Fs, Path.layer))) ) await assertSuccess(provider, [], ConfigProvider.makeRecord(new Set(["empty"]))) await assertSuccess(provider, ["empty"], undefined) }) it("preserves empty files when requested", async () => { const Fs = FileSystem.layerNoop({ readFileString(path) { return path === "/empty" ? Effect.succeed("\n") : Effect.fail( PlatformError.systemError({ module: "FileSystem", _tag: "NotFound", method: "readFileString" }) ) }, readDirectory() { return Effect.fail( PlatformError.systemError({ module: "FileSystem", _tag: "NotFound", method: "readDirectory" }) ) } }) const provider = await Effect.runPromise( ConfigProvider.fromDir({ rootPath: "/", preserveEmptyStrings: true }).pipe( Effect.provide(Layer.mergeAll(Fs, Path.layer)) ) ) await assertSuccess(provider, ["empty"], ConfigProvider.makeValue("")) }) it("orElse falls back when fromDir file is empty", async () => { const Fs = FileSystem.layerNoop({ readFileString(path) { return path === "/empty" ? Effect.succeed("") : Effect.fail( PlatformError.systemError({ module: "FileSystem", _tag: "NotFound", method: "readFileString" }) ) }, readDirectory() { return Effect.fail( PlatformError.systemError({ module: "FileSystem", _tag: "NotFound", method: "readDirectory" }) ) } }) const provider = await Effect.runPromise( ConfigProvider.fromDir({ rootPath: "/" }).pipe( Effect.map((dir) => dir.pipe(ConfigProvider.orElse(ConfigProvider.fromUnknown({ empty: "fallback" })))), Effect.provide(Layer.mergeAll(Fs, Path.layer)) ) ) await assertSuccess(provider, ["empty"], ConfigProvider.makeValue("fallback")) }) it("reads directory entries as a record", async () => { const dirs: Record> = { "/app": ["host", "port"] } const Fs = FileSystem.layerNoop({ readFileString() { return Effect.fail( PlatformError.systemError({ module: "FileSystem", _tag: "NotFound", method: "readFileString" }) ) }, readDirectory(path) { const entries = dirs[path] return entries ? Effect.succeed([...entries]) : Effect.fail( PlatformError.systemError({ module: "FileSystem", _tag: "NotFound", method: "readDirectory" }) ) } }) const provider = await Effect.runPromise( ConfigProvider.fromDir({ rootPath: "/" }).pipe(Effect.provide(Layer.mergeAll(Fs, Path.layer))) ) await assertSuccess(provider, ["app"], ConfigProvider.makeRecord(new Set(["host", "port"]))) }) it("layerAdd uses fallback", async () => { const result = await Effect.runPromise( Effect.gen(function*() { const provider = yield* ConfigProvider.ConfigProvider const secret = yield* provider.load(["secret"]) const integer = yield* provider.load(["integer"]) const fallback = yield* provider.load(["fallback"]) return { secret, integer, fallback } }).pipe(Effect.provide(AddLayer)) ) deepStrictEqual(result.secret, ConfigProvider.makeValue("shh")) deepStrictEqual(result.integer, ConfigProvider.makeValue("123")) deepStrictEqual(result.fallback, ConfigProvider.makeValue("value")) }) }) })