Skip to content

Commit

Permalink
merge ArrayFormatter / TreeFormatter into Schema
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti committed Oct 13, 2024
1 parent c0d9520 commit fe5334d
Show file tree
Hide file tree
Showing 11 changed files with 312 additions and 329 deletions.
26 changes: 20 additions & 6 deletions .changeset/six-crabs-itch.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@

Merge Schema into Effect.

### Modules

Before

```ts
import {
Arbitrary,
ArrayFormatter,
AST,
Equivalence,
FastCheck,
JSONSchema,
ParseResult,
Pretty,
Schema,
Serializable,
TreeFormatter
Serializable
} from "@effect/schema"
```

Expand All @@ -27,15 +27,29 @@ After
```ts
import {
Arbitrary,
SchemaArrayFormatter, // changed
SchemaAST, // changed
SchemaEquivalence, // changed
FastCheck,
JSONSchema,
ParseResult,
Pretty,
Schema,
Serializable,
SchemaTreeFormatter // changed
Serializable
} from "effect"
```

### Formatters

`ArrayFormatter` / `TreeFormatter` merged into `Schema` module.

Before

```ts
import { ArrayFormatter, TreeFormatter } from "@effect/schema"
```

After

```ts
import { ArrayFormatter, TreeFormatter } from "effect/Schema"
```
6 changes: 3 additions & 3 deletions packages/effect/src/Arbitrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
*/

import * as Arr from "effect/Array"
import * as FastCheck from "effect/FastCheck"
import * as Option from "effect/Option"
import * as Predicate from "effect/Predicate"
import * as FastCheck from "./FastCheck.js"
import type * as Schema from "effect/Schema"
import * as AST from "effect/SchemaAST"
import * as errors_ from "./internal/schema/errors.js"
import * as filters_ from "./internal/schema/filters.js"
import * as util_ from "./internal/schema/util.js"
import type * as Schema from "./Schema.js"
import * as AST from "./SchemaAST.js"

/**
* @category model
Expand Down
6 changes: 3 additions & 3 deletions packages/effect/src/ParseResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import { globalValue } from "effect/GlobalValue"
import * as Inspectable from "effect/Inspectable"
import * as Option from "effect/Option"
import * as Predicate from "effect/Predicate"
import { TreeFormatter } from "effect/Schema"
import type * as Schema from "effect/Schema"
import * as AST from "effect/SchemaAST"
import type { Concurrency } from "effect/Types"
import * as util_ from "./internal/schema/util.js"
import type * as Schema from "./Schema.js"
import * as AST from "./SchemaAST.js"
import * as TreeFormatter from "./SchemaTreeFormatter.js"

/**
* `ParseIssue` is a type that represents the different types of errors that can occur when decoding/encoding a value.
Expand Down
264 changes: 263 additions & 1 deletion packages/effect/src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ import * as pretty_ from "./Pretty.js"
import type { ParseOptions } from "./SchemaAST.js"
import * as AST from "./SchemaAST.js"
import * as equivalence_ from "./SchemaEquivalence.js"
import * as TreeFormatter from "./SchemaTreeFormatter.js"
import type * as Serializable from "./Serializable.js"

/**
Expand Down Expand Up @@ -9268,3 +9267,266 @@ export const Config = <A>(name: string, schema: Schema<A, string>): config_.Conf
)
)
}
// ----------------
// Formatters
// ----------------
interface Forest<A> extends ReadonlyArray<Tree<A>> {}
interface Tree<A> {
readonly value: A
readonly forest: Forest<A>
}
const makeTree = <A>(value: A, forest: Forest<A> = []): Tree<A> => ({
value,
forest
})
/**
* @category formatting
* @since 3.10.0
*/
export interface ParseResultFormatter<A> {
readonly formatIssue: (issue: ParseResult.ParseIssue) => Effect.Effect<A>
readonly formatIssueSync: (issue: ParseResult.ParseIssue) => A
readonly formatError: (error: ParseResult.ParseError) => Effect.Effect<A>
readonly formatErrorSync: (error: ParseResult.ParseError) => A
}
/**
* @category formatting
* @since 3.10.0
*/
export const TreeFormatter: ParseResultFormatter<string> = {
formatIssue: (issue) => Effect.map(formatTree(issue), drawTree),
formatIssueSync: (issue) => Effect.runSync(TreeFormatter.formatIssue(issue)),
formatError: (error) => TreeFormatter.formatIssue(error.issue),
formatErrorSync: (error) => TreeFormatter.formatIssueSync(error.issue)
}
const drawTree = (tree: Tree<string>): string => tree.value + draw("\n", tree.forest)
const draw = (indentation: string, forest: Forest<string>): string => {
let r = ""
const len = forest.length
let tree: Tree<string>
for (let i = 0; i < len; i++) {
tree = forest[i]
const isLast = i === len - 1
r += indentation + (isLast ? "└" : "├") + "─ " + tree.value
r += draw(indentation + (len > 1 && !isLast ? "│ " : " "), tree.forest)
}
return r
}
const formatTransformationKind = (kind: ParseResult.Transformation["kind"]): string => {
switch (kind) {
case "Encoded":
return "Encoded side transformation failure"
case "Transformation":
return "Transformation process failure"
case "Type":
return "Type side transformation failure"
}
}
const formatRefinementKind = (kind: ParseResult.Refinement["kind"]): string => {
switch (kind) {
case "From":
return "From side refinement failure"
case "Predicate":
return "Predicate refinement failure"
}
}
const getAnnotated = (issue: ParseResult.ParseIssue): option_.Option<AST.Annotated> =>
"ast" in issue ? option_.some(issue.ast) : option_.none()
interface CurrentMessage {
readonly message: string
readonly override: boolean
}
const getCurrentMessage = (
issue: ParseResult.ParseIssue
): Effect.Effect<CurrentMessage, cause_.NoSuchElementException> =>
getAnnotated(issue).pipe(
option_.flatMap(AST.getMessageAnnotation),
Effect.flatMap((annotation) => {
const out = annotation(issue)
return Predicate.isString(out)
? Effect.succeed({ message: out, override: false })
: Effect.isEffect(out)
? Effect.map(out, (message) => ({ message, override: false }))
: Predicate.isString(out.message)
? Effect.succeed({ message: out.message, override: out.override })
: Effect.map(out.message, (message) => ({ message, override: out.override }))
})
)
const createParseIssueGuard =
<T extends ParseResult.ParseIssue["_tag"]>(tag: T) =>
(issue: ParseResult.ParseIssue): issue is Extract<ParseResult.ParseIssue, { _tag: T }> => issue._tag === tag
const isComposite = createParseIssueGuard("Composite")
const isRefinement = createParseIssueGuard("Refinement")
const isTransformation = createParseIssueGuard("Transformation")
const getMessage: (
issue: ParseResult.ParseIssue
) => Effect.Effect<string, cause_.NoSuchElementException> = (issue: ParseResult.ParseIssue) =>
getCurrentMessage(issue).pipe(
Effect.flatMap((currentMessage) => {
const useInnerMessage = !currentMessage.override && (
isComposite(issue) ||
(isRefinement(issue) && issue.kind === "From") ||
(isTransformation(issue) && issue.kind !== "Transformation")
)
return useInnerMessage
? isTransformation(issue) || isRefinement(issue) ? getMessage(issue.issue) : option_.none()
: Effect.succeed(currentMessage.message)
})
)
const getParseIssueTitleAnnotation = (issue: ParseResult.ParseIssue): option_.Option<string> =>
getAnnotated(issue).pipe(
option_.flatMap(AST.getParseIssueTitleAnnotation),
option_.filterMap(
(annotation) => option_.fromNullable(annotation(issue))
)
)
const formatTypeMessage = (e: ParseResult.Type): Effect.Effect<string> =>
getMessage(e).pipe(
Effect.orElse(() => getParseIssueTitleAnnotation(e)),
Effect.catchAll(() =>
Effect.succeed(e.message ?? `Expected ${String(e.ast)}, actual ${util_.formatUnknown(e.actual)}`)
)
)

const getParseIssueTitle = (
issue: ParseResult.Forbidden | ParseResult.Transformation | ParseResult.Refinement | ParseResult.Composite
): string => option_.getOrElse(getParseIssueTitleAnnotation(issue), () => String(issue.ast))

const formatForbiddenMessage = (e: ParseResult.Forbidden): string => e.message ?? "is forbidden"

const formatUnexpectedMessage = (e: ParseResult.Unexpected): string => e.message ?? "is unexpected"

const formatMissingMessage = (e: ParseResult.Missing): Effect.Effect<string> =>
AST.getMissingMessageAnnotation(e.ast).pipe(
Effect.flatMap((annotation) => {
const out = annotation()
return Predicate.isString(out) ? Effect.succeed(out) : out
}),
Effect.catchAll(() => Effect.succeed(e.message ?? "is missing"))
)

const getTree = (issue: ParseResult.ParseIssue, onFailure: () => Effect.Effect<Tree<string>>) =>
Effect.matchEffect(getMessage(issue), {
onFailure,
onSuccess: (message) => Effect.succeed(makeTree(message))
})

const formatTree = (
e: ParseResult.ParseIssue | ParseResult.Pointer
): Effect.Effect<Tree<string>> => {
switch (e._tag) {
case "Type":
return Effect.map(formatTypeMessage(e), makeTree)
case "Forbidden":
return Effect.succeed(makeTree(getParseIssueTitle(e), [makeTree(formatForbiddenMessage(e))]))
case "Unexpected":
return Effect.succeed(makeTree(formatUnexpectedMessage(e)))
case "Missing":
return Effect.map(formatMissingMessage(e), makeTree)
case "Transformation":
return getTree(e, () =>
Effect.map(
formatTree(e.issue),
(tree) => makeTree(getParseIssueTitle(e), [makeTree(formatTransformationKind(e.kind), [tree])])
))
case "Refinement":
return getTree(
e,
() =>
Effect.map(
formatTree(e.issue),
(tree) => makeTree(getParseIssueTitle(e), [makeTree(formatRefinementKind(e.kind), [tree])])
)
)
case "Pointer":
return Effect.map(formatTree(e.issue), (tree) => makeTree(util_.formatPath(e.path), [tree]))
case "Composite": {
const parseIssueTitle = getParseIssueTitle(e)
return getTree(
e,
() =>
util_.isNonEmpty(e.issues)
? Effect.map(Effect.forEach(e.issues, formatTree), (forest) => makeTree(parseIssueTitle, forest))
: Effect.map(formatTree(e.issues), (tree) => makeTree(parseIssueTitle, [tree]))
)
}
}
}

/**
* @category model
* @since 3.10.0
*/
export interface ArrayIssue {
readonly _tag: ParseResult.ParseIssue["_tag"]
readonly path: ReadonlyArray<PropertyKey>
readonly message: string
}

/**
* @category formatting
* @since 3.10.0
*/
export const ArrayFormatter: ParseResultFormatter<Array<ArrayIssue>> = {
formatIssue: (issue) => formatArray(issue),
formatIssueSync: (issue) => Effect.runSync(ArrayFormatter.formatIssue(issue)),
formatError: (error) => ArrayFormatter.formatIssue(error.issue),
formatErrorSync: (error) => ArrayFormatter.formatIssueSync(error.issue)
}

const succeed = (issue: ArrayIssue) => Effect.succeed([issue])

const getArray = (
issue: ParseResult.ParseIssue,
path: ReadonlyArray<PropertyKey>,
onFailure: () => Effect.Effect<Array<ArrayIssue>>
) =>
Effect.matchEffect(getMessage(issue), {
onFailure,
onSuccess: (message) => succeed({ _tag: issue._tag, path, message })
})

const formatArray = (
e: ParseResult.ParseIssue | ParseResult.Pointer,
path: ReadonlyArray<PropertyKey> = []
): Effect.Effect<Array<ArrayIssue>> => {
const _tag = e._tag
switch (_tag) {
case "Type":
return Effect.map(formatTypeMessage(e), (message) => [{ _tag, path, message }])
case "Forbidden":
return succeed({ _tag, path, message: formatForbiddenMessage(e) })
case "Unexpected":
return succeed({ _tag, path, message: formatUnexpectedMessage(e) })
case "Missing":
return Effect.map(formatMissingMessage(e), (message) => [{ _tag, path, message }])
case "Pointer":
return formatArray(e.issue, path.concat(e.path))
case "Composite":
return getArray(e, path, () =>
util_.isNonEmpty(e.issues)
? Effect.map(Effect.forEach(e.issues, (issue) => formatArray(issue, path)), array_.flatten)
: formatArray(e.issues, path))
case "Refinement":
case "Transformation":
return getArray(e, path, () => formatArray(e.issue, path))
}
}
Loading

0 comments on commit fe5334d

Please sign in to comment.