diff --git a/.changeset/clever-waves-notice.md b/.changeset/clever-waves-notice.md new file mode 100644 index 000000000..09972e78c --- /dev/null +++ b/.changeset/clever-waves-notice.md @@ -0,0 +1,5 @@ +--- +"@effect/schema": minor +--- + +Make transformations strict by default (and allow relaxing constraints with `strict: false` option) diff --git a/README.md b/README.md index 4b21a9dc9..0c94a2064 100644 --- a/README.md +++ b/README.md @@ -1374,7 +1374,12 @@ To perform these kinds of transformations, the `@effect/schema` library provides ### transform ```ts -(from: Schema, to: Schema, decode: (b: B) => unknown, encode: (c: C) => unknown): Schema +declare const transform: ( + from: Schema, + to: Schema, + decode: (b: B) => C, + encode: (c: C) => B +) => Schema; ``` ```mermaid @@ -1404,6 +1409,22 @@ export const transformedSchema: S.Schema = In the example above, we defined a schema for the `string` type and a schema for the tuple type `[string]`. We also defined the functions `decode` and `encode` that convert a `string` into a tuple and a tuple into a `string`, respectively. Then, we used the `transform` combinator to convert the string schema into a schema for the tuple type `[string]`. The resulting schema can be used to parse values of type `string` into values of type `[string]`. +#### Non-strict option + +If you need to be less restrictive in your `decode` and `encode` functions, you can make use of the `{ strict: false }` option: + +```ts +declare const transform: ( + from: Schema, + to: Schema, + decode: (b: B) => unknown, // Less strict constraint + encode: (c: C) => unknown, // Less strict constraint + options: { strict: false } +) => Schema; +``` + +This is useful when you want to relax the type constraints imposed by the `decode` and `encode` functions, making them more permissive. + ### transformOrFail The `transformOrFail` combinator works in a similar way, but allows the transformation function to return a `ParseResult` object, which can either be a success or a failure. diff --git a/docs/modules/Schema.ts.md b/docs/modules/Schema.ts.md index 0fb92290b..f46fe5cd0 100644 --- a/docs/modules/Schema.ts.md +++ b/docs/modules/Schema.ts.md @@ -1356,16 +1356,29 @@ using the provided mapping functions. ```ts export declare const transform: { + ( + to: Schema, + decode: (b: B, options: ParseOptions, ast: AST.AST) => C, + encode: (c: C, options: ParseOptions, ast: AST.AST) => B + ): (self: Schema) => Schema ( to: Schema, decode: (b: B, options: ParseOptions, ast: AST.AST) => unknown, - encode: (c: C, options: ParseOptions, ast: AST.AST) => unknown + encode: (c: C, options: ParseOptions, ast: AST.AST) => unknown, + options: { strict: false } ): (self: Schema) => Schema + ( + from: Schema, + to: Schema, + decode: (b: B, options: ParseOptions, ast: AST.AST) => C, + encode: (c: C, options: ParseOptions, ast: AST.AST) => B + ): Schema ( from: Schema, to: Schema, decode: (b: B, options: ParseOptions, ast: AST.AST) => unknown, - encode: (c: C, options: ParseOptions, ast: AST.AST) => unknown + encode: (c: C, options: ParseOptions, ast: AST.AST) => unknown, + options: { strict: false } ): Schema } ``` @@ -1381,18 +1394,29 @@ using the provided decoding functions. ```ts export declare const transformOrFail: { + ( + to: Schema, + decode: (b: B, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult, + encode: (c: C, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult + ): (self: Schema) => Schema ( to: Schema, decode: (b: B, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult, encode: (c: C, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult, - annotations?: AST.Annotated['annotations'] + options: { strict: false } ): (self: Schema) => Schema + ( + from: Schema, + to: Schema, + decode: (b: B, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult, + encode: (c: C, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult + ): Schema ( from: Schema, to: Schema, decode: (b: B, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult, encode: (c: C, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult, - annotations?: AST.Annotated['annotations'] + options: { strict: false } ): Schema } ``` diff --git a/dtslint/Schema.ts b/dtslint/Schema.ts index f606bcbd2..cc55034e0 100644 --- a/dtslint/Schema.ts +++ b/dtslint/Schema.ts @@ -1,6 +1,7 @@ import * as Brand from "effect/Brand" import { pipe, identity } from "effect/Function"; import * as S from "@effect/schema/Schema"; +import * as ParseResult from "@effect/schema/ParseResult"; // --------------------------------------------- // From @@ -597,3 +598,35 @@ S.mutable(S.lazy(() => S.array(S.string))) // $ExpectType Schema S.mutable(S.transform(S.array(S.string), S.array(S.string), identity, identity)) + +// --------------------------------------------- +// transform +// --------------------------------------------- + +// $ExpectType Schema +S.string.pipe(S.transform(S.number, s => s.length, n => String(n))) + +// $ExpectType Schema +S.string.pipe(S.transform(S.number, s => s, n => n, { strict: false })) + +// @ts-expect-error +S.string.pipe(S.transform(S.number, s => s, n => String(n))) + +// @ts-expect-error +S.string.pipe(S.transform(S.number, s => s.length, n => n)) + +// --------------------------------------------- +// transformOrFail +// --------------------------------------------- + +// $ExpectType Schema +S.string.pipe(S.transformOrFail(S.number, s => ParseResult.success(s.length), n => ParseResult.success(String(n)))) + +// $ExpectType Schema +S.string.pipe(S.transformOrFail(S.number, s => ParseResult.success(s), n => ParseResult.success(String(n)), { strict: false })) + +// @ts-expect-error +S.string.pipe(S.transformOrFail(S.number, s => ParseResult.success(s), n => ParseResult.success(String(n)))) + +// @ts-expect-error +S.string.pipe(S.transformOrFail(S.number, s => ParseResult.success(s.length), n => ParseResult.success(n))) diff --git a/src/Schema.ts b/src/Schema.ts index 5b13fc7da..67aa0baa2 100644 --- a/src/Schema.ts +++ b/src/Schema.ts @@ -1203,32 +1203,41 @@ export function filter( * @since 1.0.0 */ export const transformOrFail: { + ( + to: Schema, + decode: (b: B, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult, + encode: (c: C, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult + ): (self: Schema) => Schema ( to: Schema, decode: (b: B, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult, encode: (c: C, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult, - annotations?: AST.Annotated["annotations"] + options: { strict: false } ): (self: Schema) => Schema + ( + from: Schema, + to: Schema, + decode: (b: B, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult, + encode: (c: C, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult + ): Schema ( from: Schema, to: Schema, decode: (b: B, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult, encode: (c: C, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult, - annotations?: AST.Annotated["annotations"] + options: { strict: false } ): Schema -} = dual(4, ( +} = dual((args) => isSchema(args[0]) && isSchema(args[1]), ( from: Schema, to: Schema, decode: (b: B, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult, - encode: (c: C, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult, - annotations?: AST.Annotated["annotations"] + encode: (c: C, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult ): Schema => make( AST.createTransform( from.ast, to.ast, - AST.createFinalTransformation(decode, encode), - annotations + AST.createFinalTransformation(decode, encode) ) )) @@ -1240,24 +1249,37 @@ export const transformOrFail: { * @since 1.0.0 */ export const transform: { + ( + to: Schema, + decode: (b: B, options: ParseOptions, ast: AST.AST) => C, + encode: (c: C, options: ParseOptions, ast: AST.AST) => B + ): (self: Schema) => Schema ( to: Schema, decode: (b: B, options: ParseOptions, ast: AST.AST) => unknown, - encode: (c: C, options: ParseOptions, ast: AST.AST) => unknown + encode: (c: C, options: ParseOptions, ast: AST.AST) => unknown, + options: { strict: false } ): (self: Schema) => Schema + ( + from: Schema, + to: Schema, + decode: (b: B, options: ParseOptions, ast: AST.AST) => C, + encode: (c: C, options: ParseOptions, ast: AST.AST) => B + ): Schema ( from: Schema, to: Schema, decode: (b: B, options: ParseOptions, ast: AST.AST) => unknown, - encode: (c: C, options: ParseOptions, ast: AST.AST) => unknown + encode: (c: C, options: ParseOptions, ast: AST.AST) => unknown, + options: { strict: false } ): Schema } = dual( - 4, + (args) => isSchema(args[0]) && isSchema(args[1]), ( from: Schema, to: Schema, - decode: (b: B, options: ParseOptions, ast: AST.AST) => unknown, - encode: (c: C, options: ParseOptions, ast: AST.AST) => unknown + decode: (b: B, options: ParseOptions, ast: AST.AST) => C, + encode: (c: C, options: ParseOptions, ast: AST.AST) => B ): Schema => transformOrFail( from, @@ -1734,7 +1756,8 @@ export const lowercase = (self: Schema): Schema self, to(self).pipe(lowercased()), (s) => s.toLowerCase(), - identity + identity, + { strict: false } ) /** @@ -1756,7 +1779,8 @@ export const trim = (self: Schema): Schema => self, to(self).pipe(trimmed()), (s) => s.trim(), - identity + identity, + { strict: false } ) /** @@ -1803,7 +1827,7 @@ export const parseJson = (self: Schema, options?: { } catch (e: any) { return ParseResult.failure(ParseResult.type(ast, u, e.message)) } - }) + }, { strict: false }) } // --------------------------------------------- @@ -2137,7 +2161,8 @@ export const clamp = self, self.pipe(to, between(min, max)), (self) => N.clamp(self, min, max), - identity + identity, + { strict: false } ) /** @@ -2172,7 +2197,8 @@ export const numberFromString = (self: Schema): Schem const n = Number(s) return isNaN(n) ? ParseResult.failure(ParseResult.type(ast, s)) : ParseResult.success(n) }, - (n) => ParseResult.success(String(n)) + (n) => ParseResult.success(String(n)), + { strict: false } ) } @@ -2311,7 +2337,8 @@ export const symbolFromString = (self: Schema): Schem self, symbolFromSelf, (s) => Symbol.for(s), - (sym) => sym.description + (sym) => sym.description, + { strict: false } ) } @@ -2506,7 +2533,8 @@ export const clampBigint = self, self.pipe(to, betweenBigint(min, max)), (self) => BigInt_.clamp(self, min, max), - identity + identity, + { strict: false } ) /** @@ -2534,7 +2562,8 @@ export const bigintFromString = (self: Schema): Schem return ParseResult.failure(ParseResult.type(ast, s)) } }, - (n) => ParseResult.success(String(n)) + (n) => ParseResult.success(String(n)), + { strict: false } ) } @@ -2565,7 +2594,8 @@ export const bigintFromNumber = (self: Schema): Schem } return ParseResult.success(Number(b)) - } + }, + { strict: false } ) } @@ -2689,7 +2719,8 @@ export const uint8ArrayFromNumbers = >( self, Uint8ArrayFromSelf, (a) => Uint8Array.from(a), - (n) => Array.from(n) + (n) => Array.from(n), + { strict: false } ) const _Uint8Array: Schema, Uint8Array> = uint8ArrayFromNumbers( @@ -2734,12 +2765,12 @@ const makeEncodingTransform = ( ParseResult.parseError([ParseResult.type(ast, s, decodeException.message)]) ), (u) => ParseResult.success(encode(u)), - { - [AST.IdentifierAnnotationId]: id, - [Internal.PrettyHookId]: (): Pretty => (u) => `${id}(${encode(u)})`, - [Internal.ArbitraryHookId]: () => arbitrary - } - ) + { strict: false } + ).pipe(annotations({ + [AST.IdentifierAnnotationId]: id, + [Internal.PrettyHookId]: (): Pretty => (u) => `${id}(${encode(u)})`, + [Internal.ArbitraryHookId]: () => arbitrary + })) /** * Transforms a base64 `string` into a `Uint8Array`. @@ -2954,7 +2985,8 @@ export const dateFromString = (self: Schema): Schema< self, ValidDateFromSelf, (s) => new Date(s), - (n) => n.toISOString() + (n) => n.toISOString(), + { strict: false } ) const _Date: Schema = dateFromString(string) @@ -3365,7 +3397,8 @@ export const data = < item, to(dataFromSelf(item)), toData, - (a) => Array.isArray(a) ? Array.from(a) : Object.assign({}, a) + (a) => Array.isArray(a) ? Array.from(a) : Object.assign({}, a), + { strict: false } ) // ---------------------------------------------