Skip to content

Commit

Permalink
Schema: Allow non-async Effects to be used with *Sync combinators, cl… (
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti authored Jan 25, 2024
1 parent 29b23a0 commit ac30bf4
Show file tree
Hide file tree
Showing 26 changed files with 264 additions and 173 deletions.
8 changes: 8 additions & 0 deletions .changeset/two-forks-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@effect/schema": patch
---

Allow non-async Effects to be used with \*Sync combinators, closes #1976

- `ParseResult`
- add `ast` and `message` fields to `Forbidden`
33 changes: 18 additions & 15 deletions packages/schema/src/ArrayFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,35 @@
import * as Option from "effect/Option"
import * as ReadonlyArray from "effect/ReadonlyArray"
import * as Format from "./Format.js"
import type { Missing, ParseError, ParseIssue, Unexpected } from "./ParseResult.js"
import { formatMessage, getMessage, getRefinementMessage, getTransformMessage } from "./TreeFormatter.js"
import type * as ParseResult from "./ParseResult.js"
import * as TreeFormatter from "./TreeFormatter.js"

/**
* @category model
* @since 1.0.0
*/
export interface Issue {
readonly _tag: ParseIssue["_tag"] | Missing["_tag"] | Unexpected["_tag"]
readonly _tag: ParseResult.ParseIssue["_tag"] | ParseResult.Missing["_tag"] | ParseResult.Unexpected["_tag"]
readonly path: ReadonlyArray<PropertyKey>
readonly message: string
}

const go = (e: ParseIssue | Missing | Unexpected, path: ReadonlyArray<PropertyKey> = []): Array<Issue> => {
const go = (
e: ParseResult.ParseIssue | ParseResult.Missing | ParseResult.Unexpected,
path: ReadonlyArray<PropertyKey> = []
): Array<Issue> => {
const _tag = e._tag
switch (_tag) {
case "Type":
return [{ _tag, path, message: formatMessage(e) }]
return [{ _tag, path, message: TreeFormatter.formatTypeMessage(e) }]
case "Forbidden":
return [{ _tag, path, message: "is forbidden" }]
return [{ _tag, path, message: TreeFormatter.formatForbiddenMessage(e) }]
case "Unexpected":
return [{ _tag, path, message: `is unexpected, expected ${Format.formatAST(e.ast, true)}` }]
case "Missing":
return [{ _tag, path, message: "is missing" }]
case "Union":
return Option.match(getMessage(e.ast, e.actual), {
return Option.match(TreeFormatter.getMessage(e.ast, e.actual), {
onNone: () =>
ReadonlyArray.flatMap(e.errors, (e) => {
switch (e._tag) {
Expand All @@ -43,7 +46,7 @@ const go = (e: ParseIssue | Missing | Unexpected, path: ReadonlyArray<PropertyKe
onSome: (message) => [{ _tag, path, message }]
})
case "Tuple":
return Option.match(getMessage(e.ast, e.actual), {
return Option.match(TreeFormatter.getMessage(e.ast, e.actual), {
onNone: () =>
ReadonlyArray.flatMap(
e.errors,
Expand All @@ -52,7 +55,7 @@ const go = (e: ParseIssue | Missing | Unexpected, path: ReadonlyArray<PropertyKe
onSome: (message) => [{ _tag, path, message }]
})
case "TypeLiteral":
return Option.match(getMessage(e.ast, e.actual), {
return Option.match(TreeFormatter.getMessage(e.ast, e.actual), {
onNone: () =>
ReadonlyArray.flatMap(
e.errors,
Expand All @@ -61,17 +64,17 @@ const go = (e: ParseIssue | Missing | Unexpected, path: ReadonlyArray<PropertyKe
onSome: (message) => [{ _tag, path, message }]
})
case "Transform":
return Option.match(getTransformMessage(e, e.actual), {
return Option.match(TreeFormatter.getTransformMessage(e, e.actual), {
onNone: () => go(e.error, path),
onSome: (message) => [{ _tag, path, message }]
})
case "Refinement":
return Option.match(getRefinementMessage(e, e.actual), {
return Option.match(TreeFormatter.getRefinementMessage(e, e.actual), {
onNone: () => go(e.error, path),
onSome: (message) => [{ _tag, path, message }]
})
case "Declaration":
return Option.match(getMessage(e.ast, e.actual), {
return Option.match(TreeFormatter.getMessage(e.ast, e.actual), {
onNone: () => go(e.error, path),
onSome: (message) => [{ _tag, path, message }]
})
Expand All @@ -82,17 +85,17 @@ const go = (e: ParseIssue | Missing | Unexpected, path: ReadonlyArray<PropertyKe
* @category formatting
* @since 1.0.0
*/
export const formatIssues = (issues: ReadonlyArray.NonEmptyReadonlyArray<ParseIssue>): Array<Issue> =>
export const formatIssues = (issues: ReadonlyArray.NonEmptyReadonlyArray<ParseResult.ParseIssue>): Array<Issue> =>
ReadonlyArray.flatMap(issues, (e) => go(e))

/**
* @category formatting
* @since 1.0.0
*/
export const formatIssue = (error: ParseIssue): Array<Issue> => formatIssues([error])
export const formatIssue = (error: ParseResult.ParseIssue): Array<Issue> => formatIssues([error])

/**
* @category formatting
* @since 1.0.0
*/
export const formatError = (error: ParseError): Array<Issue> => formatIssue(error.error)
export const formatError = (error: ParseResult.ParseError): Array<Issue> => formatIssue(error.error)
2 changes: 2 additions & 0 deletions packages/schema/src/ParseResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ export const type = InternalParser.type
*/
export interface Forbidden {
readonly _tag: "Forbidden"
readonly ast: AST.AST
readonly actual: unknown
readonly message: Option.Option<string>
}

/**
Expand Down
24 changes: 17 additions & 7 deletions packages/schema/src/Parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,14 +377,15 @@ const go = (ast: AST.AST, isDecoding: boolean): Parser => {
}
)
),
ast,
i,
options
)
} else {
const from = goMemo(AST.to(ast), true)
const to = goMemo(dropRightRefinement(ast.from), false)
return (i, options) =>
handleForbidden(InternalParser.flatMap(from(i, options), (a) => to(a, options)), i, options)
handleForbidden(InternalParser.flatMap(from(i, options), (a) => to(a, options)), ast, i, options)
}
}
case "Transform": {
Expand All @@ -411,6 +412,7 @@ const go = (ast: AST.AST, isDecoding: boolean): Parser => {
)
)
),
ast,
i1,
options
)
Expand All @@ -423,6 +425,7 @@ const go = (ast: AST.AST, isDecoding: boolean): Parser => {
handleForbidden(
InternalParser.mapError(parse(i, options ?? defaultParseOption, ast), (e) =>
InternalParser.declaration(ast, i, e)),
ast,
i,
options
)
Expand Down Expand Up @@ -1133,15 +1136,22 @@ const dropRightRefinement = (ast: AST.AST): AST.AST => AST.isRefinement(ast) ? d

const handleForbidden = <R, A>(
effect: Effect.Effect<R, ParseResult.ParseIssue, A>,
ast: AST.AST,
actual: unknown,
options?: InternalOptions
options: InternalOptions | undefined
): Effect.Effect<R, ParseResult.ParseIssue, A> => {
const eu = InternalParser.eitherOrUndefined(effect)
return eu
? eu
: options?.isEffectAllowed === true
? effect
: Either.left(InternalParser.forbidden(actual))
if (eu) {
return eu
}
if (options?.isEffectAllowed === true) {
return effect
}
try {
return Effect.runSync(Effect.either(effect as Effect.Effect<never, ParseResult.ParseIssue, A>))
} catch (e) {
return Either.left(InternalParser.forbidden(ast, actual, e instanceof Error ? e.message : undefined))
}
}

function sortByIndex<T>(
Expand Down
10 changes: 7 additions & 3 deletions packages/schema/src/TreeFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,16 @@ export const getMessage = (ast: AST.AST, actual: unknown): Option.Option<string>
}

/** @internal */
export const formatMessage = (e: ParseResult.Type): string =>
export const formatTypeMessage = (e: ParseResult.Type): string =>
getMessage(e.ast, e.actual).pipe(
Option.orElse(() => e.message),
Option.getOrElse(() => `Expected ${Format.formatAST(e.ast, true)}, actual ${Format.formatUnknown(e.actual)}`)
)

/** @internal */
export const formatForbiddenMessage = (e: ParseResult.Forbidden): string =>
Option.getOrElse(e.message, () => "is forbidden")

const getParseIsssueMessage = (
issue: ParseResult.ParseIssue,
orElse: () => Option.Option<string>
Expand Down Expand Up @@ -124,9 +128,9 @@ export const getTransformMessage = (e: ParseResult.Transform, actual: unknown):
const go = (e: ParseResult.ParseIssue | ParseResult.Missing | ParseResult.Unexpected): Tree<string> => {
switch (e._tag) {
case "Type":
return make(formatMessage(e))
return make(formatTypeMessage(e))
case "Forbidden":
return make("is forbidden")
return make(Format.formatAST(e.ast), [make(formatForbiddenMessage(e))])
case "Unexpected":
return make(`is unexpected, expected ${Format.formatAST(e.ast, true)}`)
case "Missing":
Expand Down
6 changes: 4 additions & 2 deletions packages/schema/src/internal/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,11 @@ export const type = (ast: AST.AST, actual: unknown, message?: string): ParseResu
})

/** @internal */
export const forbidden = (actual: unknown): ParseResult.Forbidden => ({
export const forbidden = (ast: AST.AST, actual: unknown, message?: string): ParseResult.Forbidden => ({
_tag: "Forbidden",
actual
ast,
actual,
message: Option.fromNullable(message)
})

/** @internal */
Expand Down
5 changes: 3 additions & 2 deletions packages/schema/test/ArrayFormatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,12 @@ describe("ArrayFormatter", () => {
})

it("Forbidden", () => {
const schema = Util.effectify(S.string)
const schema = Util.AsyncString
expectIssues(schema, "", [{
_tag: "Forbidden",
path: [],
message: "is forbidden"
message:
`Fiber #0 cannot be be resolved synchronously, this is caused by using runSync on an effect that performs async work`
}])
})

Expand Down
Loading

0 comments on commit ac30bf4

Please sign in to comment.