diff --git a/.changeset/chilled-owls-tap.md b/.changeset/chilled-owls-tap.md new file mode 100644 index 0000000000..d237d015e8 --- /dev/null +++ b/.changeset/chilled-owls-tap.md @@ -0,0 +1,5 @@ +--- +"@effect/schema": patch +--- + +Add onNoneEncoding for Schema.optional used with {as: "Option"} diff --git a/packages/schema/dtslint/Schema.ts b/packages/schema/dtslint/Schema.ts index 5a70cde364..f23be18eee 100644 --- a/packages/schema/dtslint/Schema.ts +++ b/packages/schema/dtslint/Schema.ts @@ -4,6 +4,7 @@ import * as S from "@effect/schema/Schema" import * as Brand from "effect/Brand" import { hole, identity, pipe } from "effect/Function" import * as N from "effect/Number" +import * as Option from "effect/Option" import * as Str from "effect/String" import type { Simplify } from "effect/Types" @@ -606,18 +607,46 @@ S.asSchema(S.Struct({ a: S.String.pipe(S.optional({ exact: true })) })) S.Struct({ a: S.String.pipe(S.optional({ exact: true })) }) // --------------------------------------------- -// optional() +// optional - Errors // --------------------------------------------- // @ts-expect-error S.optional(S.String, { as: "Option", default: () => "" }) +// @ts-expect-error +S.optional(S.String, { as: "Option", exact: true, onNoneEncoding: () => Option.some(null) }) + +// @ts-expect-error +S.String.pipe(S.optional({ as: "Option", exact: true, onNoneEncoding: () => Option.some(null) })) + +// @ts-expect-error +S.optional(S.String, { as: "Option", exact: true, nullable: true, onNoneEncoding: () => Option.some(1) }) + +// @ts-expect-error +S.optional(S.String, { as: "Option", onNoneEncoding: () => Option.some(null) }) + +// @ts-expect-error +S.String.pipe(S.optional({ as: "Option", onNoneEncoding: () => Option.some(null) })) + +// @ts-expect-error +S.String.pipe(S.optional({ as: "Option", exact: true, nullable: true, onNoneEncoding: () => Option.some(1) })) + +// @ts-expect-error +S.optional(S.String, { as: "Option", nullable: true, onNoneEncoding: () => Option.some(1) }) + +// @ts-expect-error +S.String.pipe(S.optional({ as: "Option", nullable: true, onNoneEncoding: () => Option.some(1) })) + // @ts-expect-error S.optional(S.String, { as: null }) // @ts-expect-error S.optional(S.String, { default: null }) +// --------------------------------------------- +// optional() +// --------------------------------------------- + // $ExpectType Schema<{ readonly a: string; readonly b: number; readonly c?: boolean | undefined; }, { readonly a: string; readonly b: number; readonly c?: boolean | undefined; }, never> S.asSchema(S.Struct({ a: S.String, b: S.Number, c: S.optional(S.Boolean) })) diff --git a/packages/schema/src/Schema.ts b/packages/schema/src/Schema.ts index 9f613fa209..2df143c957 100644 --- a/packages/schema/src/Schema.ts +++ b/packages/schema/src/Schema.ts @@ -1870,8 +1870,27 @@ export const optional: { } | { readonly as: "Option" readonly default?: never - readonly exact?: true - readonly nullable?: true + readonly exact?: never + readonly nullable?: never + readonly onNoneEncoding?: () => option_.Option + } | { + readonly as: "Option" + readonly default?: never + readonly exact?: never + readonly nullable: true + readonly onNoneEncoding?: () => option_.Option + } | { + readonly as: "Option" + readonly default?: never + readonly exact: true + readonly nullable?: never + readonly onNoneEncoding?: never + } | { + readonly as: "Option" + readonly default?: never + readonly exact: true + readonly nullable: true + readonly onNoneEncoding?: () => option_.Option } | undefined >( options?: Options @@ -1913,8 +1932,27 @@ export const optional: { } | { readonly as: "Option" readonly default?: never - readonly exact?: true - readonly nullable?: true + readonly exact?: never + readonly nullable?: never + readonly onNoneEncoding?: () => option_.Option + } | { + readonly as: "Option" + readonly default?: never + readonly exact?: never + readonly nullable: true + readonly onNoneEncoding?: () => option_.Option + } | { + readonly as: "Option" + readonly default?: never + readonly exact: true + readonly nullable?: never + readonly onNoneEncoding?: never + } | { + readonly as: "Option" + readonly default?: never + readonly exact: true + readonly nullable: true + readonly onNoneEncoding?: () => option_.Option } | undefined >( schema: Schema, @@ -1947,12 +1985,14 @@ export const optional: { readonly default?: () => A readonly nullable?: true readonly as?: "Option" + readonly onNoneEncoding?: () => option_.Option } ): PropertySignature => { const isExact = options?.exact const defaultValue = options?.default const isNullable = options?.nullable const asOption = options?.as == "Option" + const asOptionEncode = options?.onNoneEncoding ? option_.orElse(options.onNoneEncoding) : identity if (isExact) { if (defaultValue) { @@ -1983,7 +2023,10 @@ export const optional: { return optionalToRequired( NullOr(schema), OptionFromSelf(typeSchema(schema)), - { decode: option_.filter(Predicate.isNotNull), encode: identity } + { + decode: option_.filter(Predicate.isNotNull), + encode: asOptionEncode + } ) } else { return optionalToRequired( @@ -2035,13 +2078,19 @@ export const optional: { return optionalToRequired( NullishOr(schema), OptionFromSelf(typeSchema(schema)), - { decode: option_.filter((a): a is A => a != null), encode: identity } + { + decode: option_.filter((a): a is A => a != null), + encode: asOptionEncode + } ) } else { return optionalToRequired( UndefinedOr(schema), OptionFromSelf(typeSchema(schema)), - { decode: option_.filter(Predicate.isNotUndefined), encode: identity } + { + decode: option_.filter(Predicate.isNotUndefined), + encode: asOptionEncode + } ) } } else { diff --git a/packages/schema/test/Schema/optional.test.ts b/packages/schema/test/Schema/optional.test.ts index 0885e49504..5d50d23a07 100644 --- a/packages/schema/test/Schema/optional.test.ts +++ b/packages/schema/test/Schema/optional.test.ts @@ -212,6 +212,42 @@ describe("optional APIs", () => { }) }) + describe(`optionalToOption > { exact: true, nullable: true, as: "Option", onNoneEncoding: () => O.some(null) }`, () => { + it("decoding / encoding", async () => { + const schema = S.Struct({ + a: S.optional(S.NumberFromString, { + exact: true, + nullable: true, + as: "Option", + onNoneEncoding: () => O.some(null) + }) + }) + await Util.expectDecodeUnknownSuccess(schema, {}, { a: O.none() }) + await Util.expectDecodeUnknownSuccess(schema, { a: null }, { a: O.none() }) + await Util.expectDecodeUnknownSuccess(schema, { a: "1" }, { a: O.some(1) }) + await Util.expectDecodeUnknownFailure( + schema, + { + a: "a" + }, + `(Struct (Encoded side) <-> Struct (Type side)) +└─ Encoded side transformation failure + └─ Struct (Encoded side) + └─ ["a"] + └─ NumberFromString | null + ├─ Union member + │ └─ NumberFromString + │ └─ Transformation process failure + │ └─ Expected NumberFromString, actual "a" + └─ Union member + └─ Expected null, actual "a"` + ) + + await Util.expectEncodeSuccess(schema, { a: O.some(1) }, { a: "1" }) + await Util.expectEncodeSuccess(schema, { a: O.none() }, { a: null }) + }) + }) + describe(`optional > { as: "Option" }`, () => { it("decoding / encoding", async () => { const schema = S.Struct({ a: S.optional(S.NumberFromString, { as: "Option" }) }) @@ -275,6 +311,120 @@ describe("optional APIs", () => { }) }) + describe(`optional > { as: "Option", onNoneEncoding: () => O.some(undefined) }`, () => { + it("decoding / encoding", async () => { + const schema = S.Struct({ + a: S.optional(S.NumberFromString, { as: "Option", onNoneEncoding: () => O.some(undefined) }) + }) + await Util.expectDecodeUnknownSuccess(schema, {}, { a: O.none() }) + await Util.expectDecodeUnknownSuccess(schema, { a: undefined }, { a: O.none() }) + await Util.expectDecodeUnknownSuccess(schema, { a: "1" }, { a: O.some(1) }) + await Util.expectDecodeUnknownFailure( + schema, + { a: null }, + `(Struct (Encoded side) <-> Struct (Type side)) +└─ Encoded side transformation failure + └─ Struct (Encoded side) + └─ ["a"] + └─ NumberFromString | undefined + ├─ Union member + │ └─ NumberFromString + │ └─ Encoded side transformation failure + │ └─ Expected a string, actual null + └─ Union member + └─ Expected undefined, actual null` + ) + await Util.expectDecodeUnknownFailure( + schema, + { + a: "a" + }, + `(Struct (Encoded side) <-> Struct (Type side)) +└─ Encoded side transformation failure + └─ Struct (Encoded side) + └─ ["a"] + └─ NumberFromString | undefined + ├─ Union member + │ └─ NumberFromString + │ └─ Transformation process failure + │ └─ Expected NumberFromString, actual "a" + └─ Union member + └─ Expected undefined, actual "a"` + ) + + await Util.expectEncodeSuccess(schema, { a: O.some(1) }, { a: "1" }) + await Util.expectEncodeSuccess(schema, { a: O.none() }, { a: undefined }) + }) + }) + + describe(`optional > { nullable: true, as: "Option", onNoneEncoding: () => O.some(undefined) }`, () => { + it("decoding / encoding", async () => { + const schema = S.Struct({ + a: S.optional(S.NumberFromString, { nullable: true, as: "Option", onNoneEncoding: () => O.some(undefined) }) + }) + await Util.expectDecodeUnknownSuccess(schema, {}, { a: O.none() }) + await Util.expectDecodeUnknownSuccess(schema, { a: undefined }, { a: O.none() }) + await Util.expectDecodeUnknownSuccess(schema, { a: null }, { a: O.none() }) + await Util.expectDecodeUnknownSuccess(schema, { a: "1" }, { a: O.some(1) }) + await Util.expectDecodeUnknownFailure( + schema, + { + a: "a" + }, + `(Struct (Encoded side) <-> Struct (Type side)) +└─ Encoded side transformation failure + └─ Struct (Encoded side) + └─ ["a"] + └─ NumberFromString | null | undefined + ├─ Union member + │ └─ NumberFromString + │ └─ Transformation process failure + │ └─ Expected NumberFromString, actual "a" + ├─ Union member + │ └─ Expected null, actual "a" + └─ Union member + └─ Expected undefined, actual "a"` + ) + + await Util.expectEncodeSuccess(schema, { a: O.some(1) }, { a: "1" }) + await Util.expectEncodeSuccess(schema, { a: O.none() }, { a: undefined }) + }) + }) + + describe(`optional > { nullable: true, as: "Option", onNoneEncoding: () => O.some(null) }`, () => { + it("decoding / encoding", async () => { + const schema = S.Struct({ + a: S.optional(S.NumberFromString, { nullable: true, as: "Option", onNoneEncoding: () => O.some(null) }) + }) + await Util.expectDecodeUnknownSuccess(schema, {}, { a: O.none() }) + await Util.expectDecodeUnknownSuccess(schema, { a: undefined }, { a: O.none() }) + await Util.expectDecodeUnknownSuccess(schema, { a: null }, { a: O.none() }) + await Util.expectDecodeUnknownSuccess(schema, { a: "1" }, { a: O.some(1) }) + await Util.expectDecodeUnknownFailure( + schema, + { + a: "a" + }, + `(Struct (Encoded side) <-> Struct (Type side)) +└─ Encoded side transformation failure + └─ Struct (Encoded side) + └─ ["a"] + └─ NumberFromString | null | undefined + ├─ Union member + │ └─ NumberFromString + │ └─ Transformation process failure + │ └─ Expected NumberFromString, actual "a" + ├─ Union member + │ └─ Expected null, actual "a" + └─ Union member + └─ Expected undefined, actual "a"` + ) + + await Util.expectEncodeSuccess(schema, { a: O.some(1) }, { a: "1" }) + await Util.expectEncodeSuccess(schema, { a: O.none() }, { a: null }) + }) + }) + describe("optional > { exact: true, default: () => A }", () => { it("decoding / encoding", async () => { const schema = S.Struct({