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 requiredToOptional function to Schema module, closes #2881 #2882

Merged
merged 1 commit into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/mean-cycles-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/schema": patch
---

add `requiredToOptional` function to `Schema` module, closes #2881
51 changes: 48 additions & 3 deletions packages/schema/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2882,15 +2882,15 @@ console.log(Product.make({ name: "Laptop", price: 999, quantity: 2 })) // { name

### Optional Fields Primitives

The `optional` API is based on two primitives: `pptionalToOptional` and `optionalToRequired`. These primitives are incredibly useful for defining property signatures with more precision.
The `optional` API is based on two primitives: `optionalToOptional` and `optionalToRequired`. These primitives are incredibly useful for defining property signatures with more precision.

#### optionalToOptional

The `pptionalToOptional` API is used to manage the transformation from an optional field to another optional field. With this, we can control both the output type and the presence or absence of the field.
The `optionalToOptional` API is used to manage the transformation from an optional field to another optional field. With this, we can control both the output type and the presence or absence of the field.

For example a common use case is to equate a specific value in the source field with the absence of value in the destination field.

Here's the signature of the `pptionalToOptional` API:
Here's the signature of the `optionalToOptional` API:

```ts
export const optionalToOptional = <FA, FI, FR, TA, TI, TR>(
Expand Down Expand Up @@ -3000,6 +3000,51 @@ const encode = Schema.encodeSync(schema)
console.log(encode({ a: "foo" })) // Output: { a: 'foo' }
```

#### requiredToOptional

This API allows developers to specify how a field that is normally required can be treated as optional based on custom logic.

```ts
export const requiredToOptional = <FA, FI, FR, TA, TI, TR>(
from: Schema<FA, FI, FR>,
to: Schema<TA, TI, TR>,
options: {
readonly decode: (fa: FA) => Option.Option<TI>
readonly encode: (o: Option.Option<TI>) => FA
}
): PropertySignature<"?:", TA, never, ":", FI, false, FR | TR>
```

- **`from` and `to` Schemas**: Define the starting and ending schemas for the transformation.
- **`decode`**: Custom logic for transforming the required input into an optional output.
- **`encode`**: Defines how to handle the potentially optional input when encoding it back to a required output.

**Example**

Let's look at a practical example where a field `name` that is typically required can be considered optional if it's an empty string during decoding, and ensure there is always a value during encoding by providing a default.

```ts
import { Schema } from "@effect/schema"
import { Option } from "effect"

const schema = Schema.Struct({
name: Schema.requiredToOptional(Schema.String, Schema.String, {
decode: Option.liftPredicate((s) => s !== ""), // empty string is considered as absent
encode: Option.getOrElse(() => "")
})
})

const decode = Schema.decodeUnknownSync(schema)

console.log(decode({ name: "John" })) // Output: { name: 'John' }
console.log(decode({ name: "" })) // Output: {}

const encode = Schema.encodeSync(schema)

console.log(encode({ name: "John" })) // { name: 'John' }
console.log(encode({})) // Output: { name: '' }
```

### Renaming Properties

```ts
Expand Down
14 changes: 14 additions & 0 deletions packages/schema/dtslint/Context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,27 @@ S.NonEmptyArray(aContext)
// $ExpectType PropertySignature<":", string, never, ":", string, false, "aContext">
S.propertySignature(aContext)

// ---------------------------------------------
// optionalToOptional
// ---------------------------------------------

// $ExpectType PropertySignature<"?:", string, never, "?:", string, false, "aContext">
S.optionalToOptional(aContext, S.String, { decode: (o) => o, encode: (o) => o })

// ---------------------------------------------
// optionalToRequired
// ---------------------------------------------

// $ExpectType PropertySignature<":", string, never, "?:", string, false, "aContext">
S.optionalToRequired(aContext, S.String, { decode: Option.getOrElse(() => ""), encode: Option.some })

// ---------------------------------------------
// requiredToOptional
// ---------------------------------------------

// $ExpectType PropertySignature<"?:", string, never, ":", string, false, "aContext">
S.requiredToOptional(aContext, S.String, { decode: Option.some, encode: Option.getOrElse(() => "") })

// ---------------------------------------------
// optional
// ---------------------------------------------
Expand Down
34 changes: 34 additions & 0 deletions packages/schema/dtslint/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2454,3 +2454,37 @@ S.asSchema(MyTaggedStruct)

// $ExpectType [props: { readonly _tag?: "Product"; readonly name: string; readonly category?: "Electronics"; readonly price: number; }]
hole<Parameters<typeof MyTaggedStruct["make"]>>()

// ---------------------------------------------
// optionalToOptional
// ---------------------------------------------

// $ExpectType Schema<{ readonly a?: string; }, { readonly a?: string; }, "a">
S.asSchema(S.Struct({ a: S.optionalToOptional(aContext, S.String, { decode: (o) => o, encode: (o) => o }) }))

// $ExpectType Struct<{ a: PropertySignature<"?:", string, never, "?:", string, false, "a">; }>
S.Struct({ a: S.optionalToOptional(aContext, S.String, { decode: (o) => o, encode: (o) => o }) })

// ---------------------------------------------
// optionalToRequired
// ---------------------------------------------

// $ExpectType Schema<{ readonly a: string; }, { readonly a?: string; }, "a">
S.asSchema(
S.Struct({ a: S.optionalToRequired(aContext, S.String, { decode: Option.getOrElse(() => ""), encode: Option.some }) })
)

// $ExpectType Struct<{ a: PropertySignature<":", string, never, "?:", string, false, "a">; }>
S.Struct({ a: S.optionalToRequired(aContext, S.String, { decode: Option.getOrElse(() => ""), encode: Option.some }) })

// ---------------------------------------------
// requiredToOptional
// ---------------------------------------------

// $ExpectType Schema<{ readonly a?: string; }, { readonly a: string; }, "a">
S.asSchema(
S.Struct({ a: S.requiredToOptional(aContext, S.String, { decode: Option.some, encode: Option.getOrElse(() => "") }) })
)

// $ExpectType Struct<{ a: PropertySignature<"?:", string, never, ":", string, false, "a">; }>
S.Struct({ a: S.requiredToOptional(aContext, S.String, { decode: Option.some, encode: Option.getOrElse(() => "") }) })
28 changes: 27 additions & 1 deletion packages/schema/src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1812,7 +1812,7 @@ export const fromKey: {
})

/**
* Converts an optional property to a required one through a transformation `Option -> Option`.
* Converts an optional property to a required one through a transformation `Option -> Type`.
*
* - `decode`: `none` as argument means the value is missing in the input.
* - `encode`: `none` as return value means the value will be missing in the output.
Expand All @@ -1837,6 +1837,32 @@ export const optionalToRequired = <FA, FI, FR, TA, TI, TR>(
)
)

/**
* Converts an optional property to a required one through a transformation `Type -> Option`.
*
* - `decode`: `none` as return value means the value will be missing in the output.
* - `encode`: `none` as argument means the value is missing in the input.
*
* @category PropertySignature
* @since 1.0.0
*/
export const requiredToOptional = <FA, FI, FR, TA, TI, TR>(
from: Schema<FA, FI, FR>,
to: Schema<TA, TI, TR>,
options: {
readonly decode: (fa: FA) => option_.Option<TI>
readonly encode: (o: option_.Option<TI>) => FA
}
): PropertySignature<"?:", TA, never, ":", FI, false, FR | TR> =>
makePropertySignature(
new PropertySignatureTransformation(
new FromPropertySignature(from.ast, false, true, {}, undefined),
new ToPropertySignature(to.ast, true, true, {}, undefined),
option_.flatMap(options.decode),
(o) => option_.some(options.encode(o))
)
)

/**
* Converts an optional property to another optional property through a transformation `Option -> Option`.
*
Expand Down
6 changes: 5 additions & 1 deletion packages/schema/test/Schema/optionalToRequired.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ describe("optionalToRequired", () => {
const ps = S.optionalToRequired(
S.NumberFromString,
S.BigIntFromNumber,
{ decode: Option.getOrElse(() => 0), encode: Option.some }
{ decode: Option.getOrElse(() => 0), encode: Option.liftPredicate((n) => n !== 0) }
)
const schema = S.Struct({ a: ps })
await Util.expectDecodeUnknownSuccess(schema, {}, { a: 0n })
await Util.expectDecodeUnknownSuccess(schema, { a: "1" }, { a: 1n })

await Util.expectEncodeSuccess(schema, { a: 0n }, {})
await Util.expectEncodeSuccess(schema, { a: 1n }, { a: "1" })
})
})
20 changes: 20 additions & 0 deletions packages/schema/test/Schema/requiredToOptional.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as S from "@effect/schema/Schema"
import * as Util from "@effect/schema/test/TestUtils"
import * as Option from "effect/Option"
import { describe, it } from "vitest"

describe("requiredToOptional", () => {
it("two transformation schemas", async () => {
const ps = S.requiredToOptional(
S.NumberFromString,
S.BigIntFromNumber,
{ decode: Option.liftPredicate((n) => n !== 0), encode: Option.getOrElse(() => 0) }
)
const schema = S.Struct({ a: ps })
await Util.expectDecodeUnknownSuccess(schema, { a: "0" }, {})
await Util.expectDecodeUnknownSuccess(schema, { a: "1" }, { a: 1n })

await Util.expectEncodeSuccess(schema, {}, { a: "0" })
await Util.expectEncodeSuccess(schema, { a: 1n }, { a: "1" })
})
})