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

Commit

Permalink
simplify compose signature (#395)
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti authored Aug 20, 2023
1 parent e14295c commit bedba24
Show file tree
Hide file tree
Showing 7 changed files with 30 additions and 85 deletions.
2 changes: 1 addition & 1 deletion .changeset/modern-tips-smell.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
"@effect/schema": patch
---

compose: allow forcing decoding / encoding (through additional options argument)
compose: allow forcing decoding / encoding
22 changes: 1 addition & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1135,24 +1135,6 @@ In this example, we have two schemas, `schema1` and `schema2`. The first schema,
Now, by using the `compose` combinator, we can create a new schema, `composedSchema`, that combines the functionality of both `schema1` and `schema2`. This allows us to parse a string and directly obtain an array of numbers as a result.
If you need to compose two schemas, `Schema<A, B>` and `Schema<C, D>`, where `C` is a subtype of `B` then you have to add an extra option, `force: "decoding"`:
```ts
import * as S from "@effect/schema/Schema";

// $ExpectType Schema<string, unknown>
const schema1 = S.ParseJson;

// $ExpectType Schema<{ readonly a: number; }, { readonly a: number; }>
const schema2 = S.struct({ a: S.number });

// $ExpectType Schema<string, { readonly a: number; }>
const composedSchema = S.compose(schema1, schema2, {
// { readonly a: number; } is a subtype of unknown, you need to add an extra option
force: "decoding"
});
```
## InstanceOf
In the following section, we demonstrate how to use the `instanceOf` combinator to create a `Schema` for a class instance.
Expand Down Expand Up @@ -1383,9 +1365,7 @@ You can also compose the `ParseJson` schema with other schemas to refine the par
import * as S from "@effect/schema/Schema";

// $ExpectType Schema<string, { readonly a: number; }>
const schema = S.ParseJson.pipe(
S.compose(S.struct({ a: S.number }), { force: "decoding" })
);
const schema = S.ParseJson.pipe(S.compose(S.struct({ a: S.number })));
```

In this example, we've composed the `ParseJson` schema with a struct schema to ensure that the result will have a specific shape, including an object with a numeric property "a".
Expand Down
19 changes: 7 additions & 12 deletions dtslint/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -533,28 +533,23 @@ S.compose(S.split(S.string, ","), S.array(S.NumberFromString))
// $ExpectType Schema<string, readonly number[]>
S.split(S.string, ",").pipe(S.compose(S.array(S.NumberFromString)))

// $ExpectType Schema<string, readonly number[]>
S.compose(S.array(S.NumberFromString))(S.split(S.string, ","))

// decoding

// @ts-expect-error
S.compose(S.union(S.null, S.string), S.NumberFromString)

// $ExpectType Schema<string | null, number>
S.compose(S.union(S.null, S.string), S.NumberFromString, { force: 'decoding' })
S.compose(S.union(S.null, S.string), S.NumberFromString)

// $ExpectType Schema<string | null, number>
S.union(S.null, S.string).pipe(S.compose(S.NumberFromString, { force: 'decoding' }))
S.union(S.null, S.string).pipe(S.compose(S.NumberFromString))

// encoding

// @ts-expect-error
// $ExpectType Schema<string, number | null>
S.compose(S.NumberFromString, S.union(S.null, S.number))

// $ExpectType Schema<string, number | null>
S.compose(S.NumberFromString, S.union(S.null, S.number), { force: 'encoding' })
S.NumberFromString.pipe(S.compose(S.union(S.null, S.number)))

// $ExpectType Schema<string, number | null>
S.NumberFromString.pipe(S.compose(S.union(S.null, S.number), { force: 'encoding' }))
// mismatch

// @ts-expect-error
S.compose(S.string, S.number)
23 changes: 9 additions & 14 deletions src/Parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,21 +298,16 @@ const go = (ast: AST.AST, isBoundary = true): Parser<any, any> => {
}
}
case "Transform": {
const from = go(ast.from, isBoundary)
const to = go(ast.to, false)
if (isBoundary) {
const from = go(ast.from)
return (i1, options) =>
handleForbidden(
PR.flatMap(
from(i1, options),
(a) => PR.flatMap(ast.decode(a, options), (i2) => to(i2, options))
),
options
)
} else {
return (a, options) =>
handleForbidden(PR.flatMap(ast.decode(a, options), (i2) => to(i2, options)), options)
}
return (i1, options) =>
handleForbidden(
PR.flatMap(
from(i1, options),
(a) => PR.flatMap(ast.decode(a, options), (i2) => to(i2, options))
),
options
)
}
case "Declaration": {
const decode = ast.decode(...ast.typeParameters)
Expand Down
39 changes: 7 additions & 32 deletions src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -936,39 +936,14 @@ export const extend: {
* @since 1.0.0
*/
export const compose: {
<B, C>(bc: Schema<B, C>): <A>(ab: Schema<A, B>) => Schema<A, C>
<B, C extends B, D>(
cd: Schema<C, D>,
options: { force: "decoding" }
): <A>(ab: Schema<A, B>) => Schema<A, D>
<C, D>(
cd: Schema<C, D>,
options: { force: "encoding" }
): <A, B extends C>(ab: Schema<A, B>) => Schema<A, D>
<A, B, C>(ab: Schema<A, B>, bc: Schema<B, C>): Schema<A, C>
<A, B, C extends B, D>(
ab: Schema<A, B>,
cd: Schema<C, D>,
options: { force: "decoding" }
): Schema<A, D>
<A, B extends C, C, D>(
ab: Schema<A, B>,
cd: Schema<C, D>,
options: { force: "encoding" }
): Schema<A, D>
<B, C extends B, D>(bc: Schema<C, D>): <A>(ab: Schema<A, B>) => Schema<A, D>
<C, D>(bc: Schema<C, D>): <A, B extends C>(ab: Schema<A, B>) => Schema<A, D>
<A, B, C extends B, D>(ab: Schema<A, B>, cd: Schema<C, D>): Schema<A, D>
<A, B extends C, C, D>(ab: Schema<A, B>, cd: Schema<C, D>): Schema<A, D>
} = dual(
(args) => isSchema(args[1]),
(ab: any, cd: any, options?: { force: "decoding" | "encoding" }) => {
if (options) {
if (options.force === "decoding") {
return transformResult(ab, cd, P.validateResult(from(cd)), PR.success)
} else {
return transformResult(ab, cd, PR.success, P.validateResult(to(ab)))
}
} else {
return transform(ab, cd, identity, identity)
}
}
2,
<A, B, C>(ab: Schema<A, B>, cd: Schema<B, C>): Schema<A, C> =>
transform(ab, cd, identity, identity)
)

/**
Expand Down
2 changes: 1 addition & 1 deletion test/ParseJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe.concurrent("ParseJson", () => {
})

it("compose", async () => {
const schema = S.ParseJson.pipe(S.compose(S.struct({ a: S.number }), { force: "decoding" }))
const schema = S.ParseJson.pipe(S.compose(S.struct({ a: S.number })))
await Util.expectParseSuccess(schema, `{"a":1}`, { a: 1 })
await Util.expectParseFailure(schema, `{"a"}`, `Unexpected token } in JSON at position 4`)
await Util.expectParseFailure(schema, `{"a":"b"}`, `/a Expected number, actual "b"`)
Expand Down
8 changes: 4 additions & 4 deletions test/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,24 @@ describe.concurrent("compose", async () => {
})

it("force decoding: (A U B) compose (B -> C)", async () => {
const schema1 = S.compose(S.union(S.null, S.string), S.NumberFromString, { force: "decoding" })
const schema1 = S.compose(S.union(S.null, S.string), S.NumberFromString)
await Util.expectParseSuccess(schema1, "1", 1)
await Util.expectParseFailure(schema1, "a", `Expected string -> number, actual "a"`)
await Util.expectParseFailure(schema1, null, "Expected string, actual null")
const schema2 = S.union(S.null, S.string).pipe(
S.compose(S.NumberFromString, { force: "decoding" })
S.compose(S.NumberFromString)
)
await Util.expectParseSuccess(schema2, "1", 1)
await Util.expectParseFailure(schema2, "a", `Expected string -> number, actual "a"`)
await Util.expectParseFailure(schema2, null, "Expected string, actual null")
})

it("force encoding: (A -> B) compose (C U B)", async () => {
const schema1 = S.compose(S.NumberFromString, S.union(S.null, S.number), { force: "encoding" })
const schema1 = S.compose(S.NumberFromString, S.union(S.null, S.number))
await Util.expectEncodeSuccess(schema1, 1, "1")
await Util.expectEncodeFailure(schema1, null, "Expected number, actual null")
const schema2 = S.NumberFromString.pipe(
S.compose(S.union(S.null, S.number), { force: "encoding" })
S.compose(S.union(S.null, S.number))
)
await Util.expectEncodeSuccess(schema2, 1, "1")
await Util.expectEncodeFailure(schema2, null, "Expected number, actual null")
Expand Down

0 comments on commit bedba24

Please sign in to comment.