Skip to content
This repository has been archived by the owner on Jan 6, 2025. It is now read-only.

Commit

Permalink
Make transformations strict by default (and allow relaxing constraint… (
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti authored Oct 18, 2023
1 parent 2b054fd commit 135072e
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 35 deletions.
5 changes: 5 additions & 0 deletions .changeset/clever-waves-notice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/schema": minor
---

Make transformations strict by default (and allow relaxing constraints with `strict: false` option)
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1374,7 +1374,12 @@ To perform these kinds of transformations, the `@effect/schema` library provides
### transform

```ts
<A, B, C, D>(from: Schema<A, B>, to: Schema<C, D>, decode: (b: B) => unknown, encode: (c: C) => unknown): Schema<A, D>
declare const transform: <A, B, C, D>(
from: Schema<A, B>,
to: Schema<C, D>,
decode: (b: B) => C,
encode: (c: C) => B
) => Schema<A, D>;
```

```mermaid
Expand Down Expand Up @@ -1404,6 +1409,22 @@ export const transformedSchema: S.Schema<string, readonly [string]> =

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: <A, B, C, D>(
from: Schema<A, B>,
to: Schema<C, D>,
decode: (b: B) => unknown, // Less strict constraint
encode: (c: C) => unknown, // Less strict constraint
options: { strict: false }
) => Schema<A, D>;
```

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.
Expand Down
32 changes: 28 additions & 4 deletions docs/modules/Schema.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -1356,16 +1356,29 @@ using the provided mapping functions.

```ts
export declare const transform: {
<C, D, B>(
to: Schema<C, D>,
decode: (b: B, options: ParseOptions, ast: AST.AST) => C,
encode: (c: C, options: ParseOptions, ast: AST.AST) => B
): <A>(self: Schema<A, B>) => Schema<A, D>
<C, D, B>(
to: Schema<C, D>,
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 }
): <A>(self: Schema<A, B>) => Schema<A, D>
<A, B, C, D>(
from: Schema<A, B>,
to: Schema<C, D>,
decode: (b: B, options: ParseOptions, ast: AST.AST) => C,
encode: (c: C, options: ParseOptions, ast: AST.AST) => B
): Schema<A, D>
<A, B, C, D>(
from: Schema<A, B>,
to: Schema<C, D>,
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<A, D>
}
```
Expand All @@ -1381,18 +1394,29 @@ using the provided decoding functions.

```ts
export declare const transformOrFail: {
<C, D, B>(
to: Schema<C, D>,
decode: (b: B, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult<C>,
encode: (c: C, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult<B>
): <A>(self: Schema<A, B>) => Schema<A, D>
<C, D, B>(
to: Schema<C, D>,
decode: (b: B, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult<unknown>,
encode: (c: C, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult<unknown>,
annotations?: AST.Annotated['annotations']
options: { strict: false }
): <A>(self: Schema<A, B>) => Schema<A, D>
<A, B, C, D>(
from: Schema<A, B>,
to: Schema<C, D>,
decode: (b: B, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult<C>,
encode: (c: C, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult<B>
): Schema<A, D>
<A, B, C, D>(
from: Schema<A, B>,
to: Schema<C, D>,
decode: (b: B, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult<unknown>,
encode: (c: C, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult<unknown>,
annotations?: AST.Annotated['annotations']
options: { strict: false }
): Schema<A, D>
}
```
Expand Down
33 changes: 33 additions & 0 deletions dtslint/Schema.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -597,3 +598,35 @@ S.mutable(S.lazy(() => S.array(S.string)))

// $ExpectType Schema<string[], string[]>
S.mutable(S.transform(S.array(S.string), S.array(S.string), identity, identity))

// ---------------------------------------------
// transform
// ---------------------------------------------

// $ExpectType Schema<string, number>
S.string.pipe(S.transform(S.number, s => s.length, n => String(n)))

// $ExpectType Schema<string, number>
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<string, number>
S.string.pipe(S.transformOrFail(S.number, s => ParseResult.success(s.length), n => ParseResult.success(String(n))))

// $ExpectType Schema<string, number>
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)))
93 changes: 63 additions & 30 deletions src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1203,32 +1203,41 @@ export function filter<A>(
* @since 1.0.0
*/
export const transformOrFail: {
<C, D, B>(
to: Schema<C, D>,
decode: (b: B, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult<C>,
encode: (c: C, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult<B>
): <A>(self: Schema<A, B>) => Schema<A, D>
<C, D, B>(
to: Schema<C, D>,
decode: (b: B, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult<unknown>,
encode: (c: C, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult<unknown>,
annotations?: AST.Annotated["annotations"]
options: { strict: false }
): <A>(self: Schema<A, B>) => Schema<A, D>
<A, B, C, D>(
from: Schema<A, B>,
to: Schema<C, D>,
decode: (b: B, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult<C>,
encode: (c: C, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult<B>
): Schema<A, D>
<A, B, C, D>(
from: Schema<A, B>,
to: Schema<C, D>,
decode: (b: B, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult<unknown>,
encode: (c: C, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult<unknown>,
annotations?: AST.Annotated["annotations"]
options: { strict: false }
): Schema<A, D>
} = dual(4, <A, B, C, D>(
} = dual((args) => isSchema(args[0]) && isSchema(args[1]), <A, B, C, D>(
from: Schema<A, B>,
to: Schema<C, D>,
decode: (b: B, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult<unknown>,
encode: (c: C, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult<unknown>,
annotations?: AST.Annotated["annotations"]
encode: (c: C, options: ParseOptions, ast: AST.AST) => ParseResult.ParseResult<unknown>
): Schema<A, D> =>
make(
AST.createTransform(
from.ast,
to.ast,
AST.createFinalTransformation(decode, encode),
annotations
AST.createFinalTransformation(decode, encode)
)
))

Expand All @@ -1240,24 +1249,37 @@ export const transformOrFail: {
* @since 1.0.0
*/
export const transform: {
<C, D, B>(
to: Schema<C, D>,
decode: (b: B, options: ParseOptions, ast: AST.AST) => C,
encode: (c: C, options: ParseOptions, ast: AST.AST) => B
): <A>(self: Schema<A, B>) => Schema<A, D>
<C, D, B>(
to: Schema<C, D>,
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 }
): <A>(self: Schema<A, B>) => Schema<A, D>
<A, B, C, D>(
from: Schema<A, B>,
to: Schema<C, D>,
decode: (b: B, options: ParseOptions, ast: AST.AST) => C,
encode: (c: C, options: ParseOptions, ast: AST.AST) => B
): Schema<A, D>
<A, B, C, D>(
from: Schema<A, B>,
to: Schema<C, D>,
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<A, D>
} = dual(
4,
(args) => isSchema(args[0]) && isSchema(args[1]),
<A, B, C, D>(
from: Schema<A, B>,
to: Schema<C, D>,
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<A, D> =>
transformOrFail(
from,
Expand Down Expand Up @@ -1734,7 +1756,8 @@ export const lowercase = <I, A extends string>(self: Schema<I, A>): Schema<I, A>
self,
to(self).pipe(lowercased()),
(s) => s.toLowerCase(),
identity
identity,
{ strict: false }
)

/**
Expand All @@ -1756,7 +1779,8 @@ export const trim = <I, A extends string>(self: Schema<I, A>): Schema<I, A> =>
self,
to(self).pipe(trimmed()),
(s) => s.trim(),
identity
identity,
{ strict: false }
)

/**
Expand Down Expand Up @@ -1803,7 +1827,7 @@ export const parseJson = <I, A extends string>(self: Schema<I, A>, options?: {
} catch (e: any) {
return ParseResult.failure(ParseResult.type(ast, u, e.message))
}
})
}, { strict: false })
}

// ---------------------------------------------
Expand Down Expand Up @@ -2137,7 +2161,8 @@ export const clamp =
self,
self.pipe(to, between(min, max)),
(self) => N.clamp(self, min, max),
identity
identity,
{ strict: false }
)

/**
Expand Down Expand Up @@ -2172,7 +2197,8 @@ export const numberFromString = <I, A extends string>(self: Schema<I, A>): 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 }
)
}

Expand Down Expand Up @@ -2311,7 +2337,8 @@ export const symbolFromString = <I, A extends string>(self: Schema<I, A>): Schem
self,
symbolFromSelf,
(s) => Symbol.for(s),
(sym) => sym.description
(sym) => sym.description,
{ strict: false }
)
}

Expand Down Expand Up @@ -2506,7 +2533,8 @@ export const clampBigint =
self,
self.pipe(to, betweenBigint(min, max)),
(self) => BigInt_.clamp(self, min, max),
identity
identity,
{ strict: false }
)

/**
Expand Down Expand Up @@ -2534,7 +2562,8 @@ export const bigintFromString = <I, A extends string>(self: Schema<I, A>): Schem
return ParseResult.failure(ParseResult.type(ast, s))
}
},
(n) => ParseResult.success(String(n))
(n) => ParseResult.success(String(n)),
{ strict: false }
)
}

Expand Down Expand Up @@ -2565,7 +2594,8 @@ export const bigintFromNumber = <I, A extends number>(self: Schema<I, A>): Schem
}

return ParseResult.success(Number(b))
}
},
{ strict: false }
)
}

Expand Down Expand Up @@ -2689,7 +2719,8 @@ export const uint8ArrayFromNumbers = <I, A extends ReadonlyArray<number>>(
self,
Uint8ArrayFromSelf,
(a) => Uint8Array.from(a),
(n) => Array.from(n)
(n) => Array.from(n),
{ strict: false }
)

const _Uint8Array: Schema<ReadonlyArray<number>, Uint8Array> = uint8ArrayFromNumbers(
Expand Down Expand Up @@ -2734,12 +2765,12 @@ const makeEncodingTransform = <A extends string>(
ParseResult.parseError([ParseResult.type(ast, s, decodeException.message)])
),
(u) => ParseResult.success(encode(u)),
{
[AST.IdentifierAnnotationId]: id,
[Internal.PrettyHookId]: (): Pretty<Uint8Array> => (u) => `${id}(${encode(u)})`,
[Internal.ArbitraryHookId]: () => arbitrary
}
)
{ strict: false }
).pipe(annotations({
[AST.IdentifierAnnotationId]: id,
[Internal.PrettyHookId]: (): Pretty<Uint8Array> => (u) => `${id}(${encode(u)})`,
[Internal.ArbitraryHookId]: () => arbitrary
}))

/**
* Transforms a base64 `string` into a `Uint8Array`.
Expand Down Expand Up @@ -2954,7 +2985,8 @@ export const dateFromString = <I, A extends string>(self: Schema<I, A>): Schema<
self,
ValidDateFromSelf,
(s) => new Date(s),
(n) => n.toISOString()
(n) => n.toISOString(),
{ strict: false }
)

const _Date: Schema<string, Date> = dateFromString(string)
Expand Down Expand Up @@ -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 }
)

// ---------------------------------------------
Expand Down

0 comments on commit 135072e

Please sign in to comment.