diff --git a/.changeset/silly-coins-train.md b/.changeset/silly-coins-train.md new file mode 100644 index 00000000000..c1cc05a82cb --- /dev/null +++ b/.changeset/silly-coins-train.md @@ -0,0 +1,5 @@ +--- +"@effect/schema": patch +--- + +add eitherFromUnion diff --git a/packages/schema/src/Schema.ts b/packages/schema/src/Schema.ts index ffc3cc36f7e..81e45a5f7b3 100644 --- a/packages/schema/src/Schema.ts +++ b/packages/schema/src/Schema.ts @@ -3718,6 +3718,36 @@ export const either = ( }) ) +/** + * @example + * import * as Schema from "@effect/schema/Schema" + * + * // Schema<"A" | "E", Either<"E", "A">> + * Schema.eitherFromUnion(Schema.literal("A"), Schema.literal("E")) + * + * @category Either transformations + * @since 1.0.0 + */ +export const eitherFromUnion = ( + left: Schema, + right: Schema +): Schema> => { + return transformOrFail( + union(from(right), from(left)), + eitherFromSelf(to(left), to(right)), + (value, options) => + ParseResult.orElse( + ParseResult.map(Parser.parse(right)(value, options), Either.right), + () => ParseResult.map(Parser.parse(left)(value, options), Either.left) + ), + (value, options) => + Either.match(value, { + onLeft: (_) => Parser.encode(left)(_, options), + onRight: (_) => Parser.encode(right)(_, options) + }) + ) +} + const isMap = (u: unknown): u is Map => u instanceof Map const readonlyMapArbitrary = ( diff --git a/packages/schema/test/Either/eitherFromUnion.test.ts b/packages/schema/test/Either/eitherFromUnion.test.ts new file mode 100644 index 00000000000..29accbc2c0c --- /dev/null +++ b/packages/schema/test/Either/eitherFromUnion.test.ts @@ -0,0 +1,103 @@ +import * as S from "@effect/schema/Schema" +import * as Util from "@effect/schema/test/util" +import * as E from "effect/Either" +import { describe, expect, it } from "vitest" + +describe("Either/eitherFromUnion", () => { + it("property tests", () => { + Util.roundtrip(S.eitherFromUnion(S.string, S.number)) + }) + + it("decoding success", async () => { + const schema = S.eitherFromUnion(S.DateFromString, S.NumberFromString) + await Util.expectParseSuccess(schema, "1970-01-01T00:00:00.000Z", E.left(new Date(0))) + await Util.expectParseSuccess(schema, "1", E.right(1)) + + expect(E.isEither(S.decodeSync(schema)("1970-01-01T00:00:00.000Z"))).toEqual(true) + expect(E.isEither(S.decodeSync(schema)("1"))).toEqual(true) + }) + + it("decoding error", async () => { + const schema = S.eitherFromUnion(S.number, S.string) + await Util.expectParseFailure( + schema, + undefined, + `(string | number <-> Either) +└─ From side transformation failure + └─ string | number + ├─ Union member + │ └─ Expected a string, actual undefined + └─ Union member + └─ Expected a number, actual undefined` + ) + }) + + it("decoding prefer right", async () => { + const schema = S.eitherFromUnion(S.NumberFromString, S.NumberFromString) + await Util.expectParseSuccess(schema, "1", E.right(1)) + }) + + it("encoding success", async () => { + const schema = S.eitherFromUnion(S.DateFromString, S.NumberFromString) + await Util.expectEncodeSuccess(schema, E.left(new Date(0)), "1970-01-01T00:00:00.000Z") + await Util.expectEncodeSuccess(schema, E.right(1), "1") + }) + + it("encoding error", async () => { + const schema = S.eitherFromUnion( + S.compose(S.DateFromString, S.unknown), + S.compose(S.NumberFromString, S.unknown) + ) + await Util.expectEncodeFailure( + schema, + E.left(undefined), + `(string <-> Either) +└─ Transformation process failure + └─ (DateFromString <-> unknown) + └─ From side transformation failure + └─ DateFromString + └─ To side transformation failure + └─ Expected DateFromSelf, actual undefined` + ) + await Util.expectEncodeFailure( + schema, + E.right(undefined), + `(string <-> Either) +└─ Transformation process failure + └─ (NumberFromString <-> unknown) + └─ From side transformation failure + └─ NumberFromString + └─ To side transformation failure + └─ Expected a number, actual undefined` + ) + }) + + it("encoding don't overlap", async () => { + const schema = S.eitherFromUnion( + S.compose(S.DateFromString, S.unknown), + S.compose(S.NumberFromString, S.unknown) + ) + await Util.expectEncodeFailure( + schema, + E.left(1), + `(string <-> Either) +└─ Transformation process failure + └─ (DateFromString <-> unknown) + └─ From side transformation failure + └─ DateFromString + └─ To side transformation failure + └─ Expected DateFromSelf, actual 1` + ) + await Util.expectEncodeFailure( + schema, + E.right(new Date(0)), + `(string <-> Either) +└─ Transformation process failure + └─ (NumberFromString <-> unknown) + └─ From side transformation failure + └─ NumberFromString + └─ To side transformation failure + └─ Expected a number, actual ${new Date(0).toString()}` + ) + }) +})