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

Commit

Permalink
Schema: add filter overloading returning Option<ParseError> (#629)
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti authored Dec 5, 2023
1 parent 80c56b9 commit f690ebe
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 8 deletions.
7 changes: 7 additions & 0 deletions .changeset/nine-baboons-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@effect/schema": patch
---

Schema: add filter overloading returning Option<ParseError>

For more complex scenarios, you can return an `Option<ParseError>` type instead of a boolean. In this context, `None` indicates success, and `Some(error)` rejects the input with a specific error
39 changes: 36 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -905,9 +905,9 @@ S.templateLiteral(S.union(EmailLocaleIDs, FooterLocaleIDs), S.literal("_id"));

## Filters

`@effect/schema/Schema` lets you provide custom validation logic via _filters_.
In the `@effect/schema/Schema` library, you can apply custom validation logic using _filters_.

You can define a custom validation check on any schema with `filter`:
You can define a custom validation check on any schema using the `filter` function. Here's a simple example:

```ts
import * as S from "@effect/schema/Schema";
Expand All @@ -926,7 +926,7 @@ error(s) found
*/
```

It is good practice to add as much metadata as possible so that it can be used later by introspecting the schema.
It's recommended to include as much metadata as possible for later introspection of the schema, such as an identifier, JSON schema representation, and a description:

```ts
const LongString = S.string.pipe(
Expand All @@ -940,6 +940,39 @@ const LongString = S.string.pipe(
);
```

For more complex scenarios, you can return an `Option<ParseError>` type instead of a boolean. In this context, `None` indicates success, and `Some(error)` rejects the input with a specific error. Here's an example:

```ts
import * as ParseResult from "@effect/schema/ParseResult";
import * as S from "@effect/schema/Schema";

const schema = S.struct({ a: S.string, b: S.string }).pipe(
S.filter((o) =>
o.b === o.a
? Option.none()
: Option.some(
ParseResult.parseError([
ParseResult.key("b", [
ParseResult.type(
S.literal(o.a).ast,
o.b,
`should be equal to a's value ("${o.a}")`
),
]),
])
)
)
);

console.log(S.parseSync(schema)({ a: "a", b: "b" }));
/*
throws:
error(s) found
└─ ["b"]
└─ should be equal to a's value ("a")
*/
```

> [!WARNING]
> Please note that the use of filters do not alter the type of the `Schema`. They only serve to add additional constraints to the parsing process.
Expand Down
4 changes: 4 additions & 0 deletions docs/modules/Schema.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -1673,6 +1673,10 @@ Added in v1.0.0
**Signature**
```ts
export declare function filter<A>(
f: (a: A, options: ParseOptions, self: AST.AST) => Option.Option<ParseResult.ParseError>,
options?: FilterAnnotations<A>
): <I>(self: Schema<I, A>) => Schema<I, A>
export declare function filter<C extends A, B extends A, A = C>(
refinement: Predicate.Refinement<A, B>,
options?: FilterAnnotations<A>
Expand Down
21 changes: 16 additions & 5 deletions src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1190,6 +1190,10 @@ export const lazy = <I, A = I>(
* @category combinators
* @since 1.0.0
*/
export function filter<A>(
f: (a: A, options: ParseOptions, self: AST.AST) => Option.Option<ParseResult.ParseError>,
options?: FilterAnnotations<A>
): <I>(self: Schema<I, A>) => Schema<I, A>
export function filter<C extends A, B extends A, A = C>(
refinement: Predicate.Refinement<A, B>,
options?: FilterAnnotations<A>
Expand All @@ -1199,16 +1203,23 @@ export function filter<B extends A, A = B>(
options?: FilterAnnotations<A>
): <I>(self: Schema<I, B>) => Schema<I, B>
export function filter<A>(
predicate: Predicate.Predicate<A>,
predicate:
| Predicate.Predicate<A>
| ((a: A, options: ParseOptions, self: AST.AST) => Option.Option<ParseResult.ParseError>),
options?: FilterAnnotations<A>
): <I>(self: Schema<I, A>) => Schema<I, A> {
return (self) =>
make(AST.createRefinement(
self.ast,
(a: A, _, ast: AST.AST) =>
predicate(a)
? Option.none()
: Option.some(ParseResult.parseError([ParseResult.type(ast, a)])),
(a: A, options, ast: AST.AST) => {
const out = predicate(a, options, ast)
if (Predicate.isBoolean(out)) {
return out
? Option.none()
: Option.some(ParseResult.parseError([ParseResult.type(ast, a)]))
}
return out
},
toAnnotations(options)
))
}
Expand Down
28 changes: 28 additions & 0 deletions test/Schema/filter.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import * as AST from "@effect/schema/AST"
import * as ParseResult from "@effect/schema/ParseResult"
import * as S from "@effect/schema/Schema"
import * as Util from "@effect/schema/test/util"
import * as Option from "effect/Option"
import { describe, expect, it } from "vitest"

describe("Schema/filter", () => {
Expand Down Expand Up @@ -30,4 +33,29 @@ describe("Schema/filter", () => {
[AST.TitleAnnotationId]: "title"
})
})

it("Option overloading", async () => {
const schema = S.struct({ a: S.string, b: S.string }).pipe(
S.filter((o) =>
o.b === o.a
? Option.none()
: Option.some(
ParseResult.parseError([
ParseResult.key("b", [
ParseResult.type(S.literal(o.a).ast, o.b, `should be equal to a's value ("${o.a}")`)
])
])
)
)
)

await Util.expectParseSuccess(schema, { a: "x", b: "x" })
await Util.expectParseFailureTree(
schema,
{ a: "a", b: "b" },
`error(s) found
└─ ["b"]
└─ should be equal to a's value ("a")`
)
})
})

0 comments on commit f690ebe

Please sign in to comment.