From e3766411b60ebb45d31e9c9d94efa099121d4d58 Mon Sep 17 00:00:00 2001 From: Giulio Canti Date: Wed, 22 May 2024 21:06:02 +0200 Subject: [PATCH] Add support for `Config` module, closes #2346 (#2816) --- .changeset/hungry-eyes-doubt.md | 5 ++ packages/schema/src/Schema.ts | 18 ++++++ .../schema/test/Schema/Config/Config.test.ts | 59 +++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 .changeset/hungry-eyes-doubt.md create mode 100644 packages/schema/test/Schema/Config/Config.test.ts diff --git a/.changeset/hungry-eyes-doubt.md b/.changeset/hungry-eyes-doubt.md new file mode 100644 index 0000000000..9ff9bde200 --- /dev/null +++ b/.changeset/hungry-eyes-doubt.md @@ -0,0 +1,5 @@ +--- +"@effect/schema": patch +--- + +Add support for `Config` module, closes #2346 diff --git a/packages/schema/src/Schema.ts b/packages/schema/src/Schema.ts index fcb7efe46a..74b8df3ae5 100644 --- a/packages/schema/src/Schema.ts +++ b/packages/schema/src/Schema.ts @@ -9,6 +9,8 @@ import * as boolean_ from "effect/Boolean" import type { Brand } from "effect/Brand" import * as cause_ from "effect/Cause" import * as chunk_ from "effect/Chunk" +import * as config_ from "effect/Config" +import * as configError_ from "effect/ConfigError" import * as data_ from "effect/Data" import * as duration_ from "effect/Duration" import * as Effect from "effect/Effect" @@ -47,6 +49,7 @@ import * as util_ from "./internal/util.js" import * as ParseResult from "./ParseResult.js" import * as pretty_ from "./Pretty.js" import type * as Serializable from "./Serializable.js" +import * as TreeFormatter from "./TreeFormatter.js" /** * @since 1.0.0 @@ -8056,3 +8059,18 @@ export class BooleanFromUnknown extends transform( static override annotations: (annotations: Annotations.Schema) => typeof BooleanFromUnknown = super .annotations } + +/** + * @category Config validations + * @since 1.0.0 + */ +export const Config = (name: string, schema: Schema): config_.Config => { + const decodeEither_ = decodeEither(schema) + return config_.string(name).pipe( + config_.mapOrFail((a) => + decodeEither_(a).pipe( + either_.mapLeft((error) => configError_.InvalidData([], TreeFormatter.formatErrorSync(error))) + ) + ) + ) +} diff --git a/packages/schema/test/Schema/Config/Config.test.ts b/packages/schema/test/Schema/Config/Config.test.ts new file mode 100644 index 0000000000..3d5b3b44ff --- /dev/null +++ b/packages/schema/test/Schema/Config/Config.test.ts @@ -0,0 +1,59 @@ +import * as Schema from "@effect/schema/Schema" +import type * as Config from "effect/Config" +import * as ConfigError from "effect/ConfigError" +import * as ConfigProvider from "effect/ConfigProvider" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import { describe, expect, it } from "vitest" + +/** + * Asserts that loading a configuration with invalid data fails with the expected error. + * + * @param config - The configuration to load. + * @param map - The map of configuration values. + * @param error - The expected error. + */ +const assertFailure = ( + config: Config.Config, + map: ReadonlyArray, + error: ConfigError.ConfigError +) => { + const configProvider = ConfigProvider.fromMap(new Map(map)) + const result = Effect.runSync(Effect.exit(configProvider.load(config))) + expect(result).toStrictEqual(Exit.fail(error)) +} + +/** + * Asserts that loading a configuration with valid data succeeds and returns the expected value. + * + * @param config - The configuration to load. + * @param map - The map of configuration values. + * @param a - The expected value. + */ +const assertSuccess = ( + config: Config.Config, + map: ReadonlyArray, + a: A +) => { + const configProvider = ConfigProvider.fromMap(new Map(map)) + const result = Effect.runSync(Effect.exit(configProvider.load(config))) + expect(result).toStrictEqual(Exit.succeed(a)) +} + +describe("Config", () => { + it("should validate the configuration schema correctly", () => { + const config = Schema.Config("A", Schema.NonEmpty) + assertSuccess(config, [["A", "a"]], "a") + assertFailure(config, [], ConfigError.MissingData(["A"], `Expected A to exist in the provided map`)) + assertFailure( + config, + [["A", ""]], + ConfigError.InvalidData( + ["A"], + `NonEmpty +└─ Predicate refinement failure + └─ Expected NonEmpty (a non empty string), actual ""` + ) + ) + }) +})