Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add onNoneEncoding to schema.optional #2772

Merged
5 changes: 5 additions & 0 deletions .changeset/chilled-owls-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/schema": patch
---

Add onNoneEncoding for Schema.optional used with {as: "Option"}
65 changes: 57 additions & 8 deletions packages/schema/src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<undefined>
} | {
readonly as: "Option"
readonly default?: never
readonly exact?: never
readonly nullable: true
readonly onNoneEncoding?: option_.Option<null | undefined>
} | {
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<null>
} | undefined
>(
options?: Options
Expand Down Expand Up @@ -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<undefined>
} | {
readonly as: "Option"
readonly default?: never
readonly exact?: never
readonly nullable: true
readonly onNoneEncoding?: option_.Option<null | undefined>
} | {
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<null>
} | undefined
>(
schema: Schema<A, I, R>,
Expand All @@ -1933,7 +1971,7 @@ export const optional: {
| (Types.Has<Options, "as"> extends true ? option_.Option<A> : A)
| (Types.Has<Options, "as" | "default" | "exact"> extends true ? never : undefined),
never,
"?:",
Types.Has<Options, "onNoneEncoding"> extends true ? ":" : "?:",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the same applies here, it should be "?:" as before

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad, sorry! I'm fixing that quickly
Why not having define types that are re-used in both overloads? It feels a bit painful to have to update both every time but there might be some reasons I don't know of.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, now that the type is quite big, it makes even more sense to define the type only once

| I
| (Types.Has<Options, "nullable"> extends true ? null : never)
| (Types.Has<Options, "exact"> extends true ? never : undefined),
Expand All @@ -1947,12 +1985,14 @@ export const optional: {
readonly default?: () => A
readonly nullable?: true
readonly as?: "Option"
readonly onNoneEncoding?: option_.Option<null | undefined>
}
): PropertySignature<any, any, never, any, any, boolean, any> => {
const isExact = options?.exact
const defaultValue = options?.default
const isNullable = options?.nullable
const asOption = options?.as == "Option"
const onNoneEncoding = options?.onNoneEncoding ?? option_.none()

if (isExact) {
if (defaultValue) {
Expand Down Expand Up @@ -1983,7 +2023,10 @@ export const optional: {
return optionalToRequired(
NullOr(schema),
OptionFromSelf(typeSchema(schema)),
{ decode: option_.filter(Predicate.isNotNull<A | null>), encode: identity }
{
decode: option_.filter(Predicate.isNotNull<A | null>),
encode: option_.orElse(() => onNoneEncoding as option_.Option<null>)
}
)
} else {
return optionalToRequired(
Expand Down Expand Up @@ -2035,13 +2078,19 @@ export const optional: {
return optionalToRequired(
NullishOr(schema),
OptionFromSelf(typeSchema(schema)),
{ decode: option_.filter<A | null | undefined, A>((a): a is A => a != null), encode: identity }
{
decode: option_.filter<A | null | undefined, A>((a): a is A => a != null),
encode: option_.orElse(() => onNoneEncoding)
}
)
} else {
return optionalToRequired(
UndefinedOr(schema),
OptionFromSelf(typeSchema(schema)),
{ decode: option_.filter(Predicate.isNotUndefined<A | undefined>), encode: identity }
{
decode: option_.filter(Predicate.isNotUndefined<A | undefined>),
encode: option_.orElse(() => onNoneEncoding as option_.Option<undefined>)
}
)
}
} else {
Expand Down
145 changes: 145 additions & 0 deletions packages/schema/test/Schema/optional.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,37 @@ 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" }) })
Expand Down Expand Up @@ -275,6 +306,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({
Expand Down